Laravel Validation Request & API route POST parameter - laravel

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 ?
Pass the id in the body ? Seems messy
Use Validator to validate manually ? I would prefer to keep Form Validation
Remove the tenantId rule and check it manually ?
Any suggestions ?
Thank you

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.

So the solution I found to trigger 404 is the following :
Remove the tenantId from EmailSettings Validation
Add a provider to add a custom error when the exception 'ModelNotFoundException' occurs like here
No query results for model in Laravel with Dingo - how to make a RESTful response on failure?
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);
}

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;
}

Related

laravel testing web routes

I have a web route for manufactures module (Backed route that handle resource)
Route::resource('/admin/manufactures', App\Http\Controllers\Back\ManufacturerController::class);
I have create a create a ManufacturerRequest with a simple rule name=>required
and i want to use Laravel Test to test the resource (CRUD)
In my controller the store method is as follow
public function store(ManufacturerRequest $request)
{
//db
$request->validate();
Manufacturer::create($request->all());
}
I have a simple test
$response = $this->post('/admin/manufactures' ,
[
'_token' => csrf_token(),
'name' => 'test'
]);
$response->assertStatus(200);
which is return 403, but besides that the store Method takes ManufacturerRequest object that handles validation, and in the case of the test i pass an array because it only accepts array.
So how can I create a Test that "simulate" form and pass request to controller in order to test validation and CRUD
What you want to do is very easy and is also explained on the Documentation, it is very important that you fully read the documentation so you have a rough idea of what you can do with the framework, specially because you are new with it.
As you did not specify which version you are using, I will be using Laravel 8, but it is roughly the same across the board.
Based on your code:
Resource route
Route::resource('/admin/manufactures', ManufacturerController::class);
Controller
public function store(ManufacturerRequest $request)
{
//db
$request->validate();
Manufacturer::create($request->all());
}
You need to change your controller to:
public function store(ManufacturerRequest $request)
{
//db
Manufacturer::create($request->all());
}
Yes, just remove the $request->validate(); as the framework will automatically resolve the FormRequest and authorize and validate. If you read part of the validate explanation you will see this:
So, how are the validation rules evaluated? All you need to do is type-hint the request on your controller method. The incoming form request is validated before the controller method is called, meaning you do not need to clutter your controller with any validation logic.
So, when the first line of the controller is run, it means the FormRequest passed the authorization check and validated the input.
What you can also update on your controller is:
public function store(ManufacturerRequest $request)
{
//db
Manufacturer::create($request->validated());
}
See I have changed $request->all() with $request->validated(), validated will only return the fields you have a key on the FormRequest's rules, if you use all you will be passing everything you have on the request (also passing non-validated data and that is not good).
Before you try anything, I recommend you read my answer on this post, so you can have a clearer picture about testing.
So, you are getting a 403 maybe because you have a middleware asking for you to be logged in, and you did not use $this->actingAs().
Just because you did not share the FormRequest rules, I will just give a super small example. If you have a this rule inside:
'name' => ['required', 'string'],
What you can do to test that is:
public function test_manufacturer_is_created(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/admin/manufactures', ['name' => $name = 'Manufacturer 1']);
$response->assertSuccessful();
$this->assertDatabaseHas(
'manufacturers',
[
'name' => $name
]
);
}
/**
* #depends test_manufacturer_is_created
*/
public function test_unauthorized_error_is_thrown_when_the_user_is_not_logged_in(): void
{
$response = $this->post('/admin/manufactures', ['name' => 'Manufacturer 1']);
$response->assertUnauthorized();
}
/**
* #depends test_manufacturer_is_created
* #dataProvider invalidDataProvider
*/
public function test_error_should_be_returned_when_invalid_data_is_sent($value, bool $deleteField): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post(
'/admin/manufactures',
! $deleteField ? ['name' => $value] : []
);
$response->assertInvalid(['name']);
}
public function invalidDataProvider(): array
{
return [
'Missing name' => [null, true],
'Empty name' => ['', false],
'Null name' => [null, false],
'Array name' => [[], false],
'Number name' => [123, false],
'Boolean name' => [true, false],
];
}
Have in mind I used a lot of things in here:
I have tested if the normal insertion works, if it is checking the a valid name is input (FormRequest rules) and that if the user is not logged in it should throw an unauthorized exception.
I have used #depends, that is used to run tests ONLY if the dependant test passes, that way we can prevent running the "negative" tests just because the normal flow did not succeed, so it makes no sense to run the other ones and also get a "the test did not pass".
I have also used #dataProvider, that is used to share data with a test, so instead of copy-pasting the test a lot of times with data variation, you just vary what data you want the test to use.

Laravel - how to retrieve url parameter in custom Request?

I need to make custom request and use its rules. Here's what I have:
public function rules()
{
return [
'name' => 'required|min:2',
'email' => 'required|email|unique:users,email,' . $id,
'password' => 'nullable|min:4'
];
}
The problem is that I can't get $id from url (example.com/users/20), I've tried this as some forums advised:
$this->id
$this->route('id')
$this->input('id')
But all of this returns null, how can I get id from url?
When you are using resources, the route parameter will be named as the singular version of the resource name. Try this.
$this->route('user');
Bonus points
This sound like you are going down the path of doing something similar to this.
User::findOrFail($this->route('user'));
In the context of controllers this is an anti pattern, as Laravels container automatic can resolve these. This is called model binding, which will both handle the 404 case and fetching it from the database.
public function update(Request $request, User $user) {
}

Data validation in Laravel is not working

I've created a custom request in my project, but somehow it is not working. I'm facing two errors. I'm trying to show a message on view if validation fails through Ajax.
1) 422 Unprocessable Entity error
and
2) Undefined variable: teacherrequest
validation rules which i set in Request folder,
TeacherRequest.php:
public function rules()
{
return [
'Name' => 'required|regex:/^[\pL\s\-]+$/u',
'FName' => 'required|regex:/^[\pL\s\-]+$/u',
];
}
Controller:
public function update(TeacherRequest $request, $id)
{
if ($teacherrequest->fails()) {
return response()->json([
'msg' => 'Please Enter Correct Data',
]);
}
}
AJAX:
success: function (data) {
if(data.msg){
alert("please validate data");
}
}
Update:
if i remove if condition, i am getting 422 error, how to show that on view?
First, public function update(TeacherRequest $request) so in the function you need to use $request not $teacherrequest.
And second You need to have public function authorize() returning true.
You define TeacherRequest as $request TeacherRequest $request
but in next line use it as
if ($teacherrequest->fails()){ // this is wrong
correct one should be define like this
TeacherRequest $teacherrequest
or if you didn't change the dependency injection, just change the validator like this
if ($request->fails()){
summary: why error happened already specifically explained which is undefined teacherrequest variable, therefore 2 solutions above can solve it
If you type hint your request class as a parameter in your controller's action method (like you are doing in the example above) Laravel will automatically run your validation rules and return a 422 Unprocessable Entity if validation fails. You don't need to manually check if validation fails like you're doing above; in your controller's update method you can just implement the logic you want to run when validation passes.
Also on your front end you will need to use the error ajax callback to display the error message because a 422 status code is not considered successful.
See the documentation on creating form requests.
So, how are the validation rules evaluated? All you need to do is type-hint the request on your controller method. The incoming form request is validated before the controller method is called, meaning you do not need to clutter your controller with any validation logic.

What are the benefits of using Laravel Request Class in API's development?

Is there any benefit of using laravel requests classes for store and update methods in developing restful API's? Or do I have to make custom Validator::make response?
I have been facing difficulty in modifying the response format from failed requests as to follow some development standards requirements.
Can we modify the failed responses format from request class for API's?
I prefer to use independent from request class because there is at least one benefit: more clear code.
you can generate response as you wish like this (this is my solution, maybe there are more better solutions. i use this solution to return only one validation error not all. you can modify it as you wish):
in your Form request class add this method:
protected function failedValidation(Validator $validator)
{
$this->validator = $validator;
foreach ($validator->messages()->getMessages() as $key => $value) {
$first_messages_only[$key] = $value[0];
}
throw new ValidationException($first_messages_only);
}
and then in your Exception handler class, write this block of code in your render() method:
if ($exception instanceof ValidationException) {
$response = [
'status' => false,
'message' => trans('api.general.validation_not_passed'), // $exception->getMessage()
'data' => ['validation_errors' => $exception->validator]
];
return response()->json($response);
}
Since you asked its usage in API development then you can easily tell request class that you want json response by adding application/json header in your request then it will return json response.
Request class is best approach to validate incoming input from user which provides a lot of other features as well.
In Request class you can write validation rules for all request types e.g. get,post,put|patch or delete
You can allow or disallow anyone using authorize method based on your project logic.
You can write custom messages and send them custom error message bags.
If you write whole thing in a controller method then that will not be a good approach and difficult to manage while request class will make you comfortable while dealing with validations only.
protected $errorBag = 'custom_errors_bag'
public function authorize()
{
return true; //or any other logic here to authorize the user
}
public function rules()
{
switch ($this->method()){
case 'POST':
return [
'username' => 'required|string|max:20|unique:users',
//...
];
case 'PUT':
case 'PATCH':
return [
'username' => 'required|string|max:20|unique:users,id',
//...
];
case 'DELETE':[
'id' => 'required'
//...
];
default:break;
}
}
public function messages()
{
return [
'username.required' => 'Please provide username',
'username.unique' => 'Username must be unique',
//...
];
}

How can I use Laravel Validation in the API, it's returning my view now?

I have this route in route/api.php:
Route::post('/register', 'LoginController#register');
My LoginController
class LoginController extends Controller {
public function register(Request $request) {
$this->validate($request, // <-- using this will return the view
// from **web.php** instead of the expected json response.
[
'email' => 'required|email',
'firstName' => 'required|alpha_dash',
'lastName' => 'required',
'password' => 'required|confirmed',
]);
$input = $request->all();
//$plain_password = $input['password'];
$input['uuid'] = uuid();
$input['password'] = Hash::make($input['password']);
$user = User::create($input);
dd($errors);
$response['succes'] = true;
$response['user'] = $user;
return response($response);
}
}
Why does adding a validation call change the behaviour to returning my view / the wrong route. I want the api to validate my request too, not just my "frontend".
When you use Laravel's validate method from controller it automatically handles/takes the step if the validation fails. So, depending on the required content type/request type, it determines whether to redirect back or to a given url or sending a json response. Ultimately, something like thos following happens when your validation fails:
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return new JsonResponse($errors, 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
So, if the first if statement is true then you'll get a json response and it'll be true if you either send an ajax request or if you attach a accept header with your request to accept json response (When requesting from a remote server). So, make sure your request fulfills the requirements.
Alternatively, you can manually validate the request using the Validator component and return a json response explicitly if it fails.

Resources