Reputation: 1
I'm facing a little issue with Form Request Validation and how to handle it with one API route.
The resource that I need to create depends on an other resource.
(Here an EmailSettings
belongs to a Tenant)
So the look of my route should be something like : /api/tenants/{id}/email_settings
And my request validation expects several fields including the tenantId
:
public function rules() {
return [
'email' => 'bail|required|email|unique:email_settings,email',
'name' => 'bail|required',
'username' => 'bail|required',
'password' => 'bail|required'
'imapHost' => 'bail|required',
'imapPort' => 'bail|required',
'imapEncryption' => 'bail|required',
'imapValidateCert' => 'bail|required',
'smtpHost' => 'bail|required',
'smtpPort' => 'bail|required',
'smtpEncryption' => 'bail|required',
'tenantId' => 'bail|required',
];
}
And I send the request like this :
try {
const response = await this.tenantForm.post('/api/tenants')
let newTenant = helpers.getNewResourceFromResponseHeaderLocation(response)
let tenantId = parseInt(newTenant.id);
try {
await this.emailSettingsForm.post('/api/tenants/' + tenantId + '/email_settings')
this.requestAllTenants()
} catch ({response}) {
$('.second.modal').modal({blurring: true, closable: false}).modal('show');
}
} catch ({response}) {
$('.first.modal').modal({blurring: true}).modal('show');
}
So the tenantId
is passed as a parameter and not in the request body to respect the REST convention.
But the problem is in my Controller, when I merge the data to create the resource, the validation has already took place only on body data before the merge.
public function store(EmailSettingValidation $request, $tenant_id) {
$emailSetting = $this->emailSettingService->create(
array_merge($request->all(), compact($tenant_id))
);
return $this->response->created($emailSetting);
}
So what is the best way to handle it properly?
tenantId
rule and check it manually?Any suggestions?
Upvotes: 0
Views: 2391
Reputation: 5552
If you define your api route like this:
Roue::post('tenants/{tenant}/emails_settings', 'Controller@store');
and modify your controller method to type-hint the model with a variable name that matches your route definition:
public function store(EmailSettingValidation $request, Tenant $tenant) {}
then Laravel will automatically find the Tenant by ID and inject it into the controller, throwing a ModelNotFoundException (404) if it doesn't exist. That should take care of validating the id.
Authorization is another matter.
Upvotes: 1
Reputation: 2789
Travis Britz and Guillaumehanotel each have half of your answer, but you're still missing a detail.
From Travis Britz- Yes, include the tenant_id on the URI so it gets injected into the controller. From Guillaumehanotel- Also used the Eloquent findOrFail in that Id in your Controller (or whatever class the Controller is leveraging to do this, like a Repository or Service class).
The last piece you're missing though is handling the error. You can do this in the Controller if you like, but I generally like making it a rule for my entire system that the Illuminate\Database\Eloquent\ModelNotFoundException
Exceptions that come out of findOrFail()
should always result in a 404.
Go to app/Exceptions/Handler.php
. I'm pretty sure Laravel auto-generates a meat and potatoes version of this file for you, but if you don't already have one, it should look something like this:
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
/**
* Class Handler
* @package App\Exceptions
*/
class Handler extends ExceptionHandler
{
/**
* Render an exception into an HTTP response.
*
* For our API, we need to override the call
* to the parent.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $error)
{
$exception = [
'title' => 'Internal Error',
'message' => $error->getMessage();
];
$statusCode = 500;
$headers = [
'Content-Type', 'application/json'
];
return response()->json($exception, $statusCode, $headers, JSON_PRETTY_PRINT);
}
}
Laravel basically has a system-wide try/catch
that sends all errors through here first. That's how errors get rendered into something the browser can actually interpret when you're in debug-mode, rather than just kill the process outright. This also gives you the opportunity to apply a few special rules.
So all you need to do is tell Handler::render()
to change the default error code that occurs when it sees the type of error that can only come from findOrFail()
. (This kind of thing is why it's always good to make your own 'named exceptions', even if they do absolutely nothing except inherit the base \Exception
class.)
Just add this just before render()
returns anything:
if ($error instanceof Illuminate\Database\Eloquent\ModelNotFoundException) {
$statusCode = 404;
}
Upvotes: 0
Reputation: 1
So the solution I found to trigger 404 is the following :
Try to throw this Exception with the findOrFail method if invalid ID :
public function store(EmailSettingValidation $request, $tenant_id) {
Tenant::findOrFail($tenant_id);
$emailSetting = $this->emailSettingService->create(
array_merge($request->all(), ['tenantId' => $tenant_id])
);
return $this->response->created($emailSetting);
}
Upvotes: 0