loading...

Laravel Form Request Tips & Tricks.

secmohammed profile image mohammed osama ・11 min read

First things first, If you don't understand laravel's form request, or you haven't got your legs wet yet. It's super easy to understand.

In essence, we use the laravel form request to validate the incoming request to your endpoint but abstracted out of your controller, which is neater than validating the request at the controller's method. this opens the ability to reuse the validation rules as they're abstracted away.
Laravel provides you with the ability to create a form request validation throughout its artisan command.

php artisan make:request UpdatePostFormRequest

Alright, Let's destruct the output class of this command piece by piece to understand what's there.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostFormRequest extends FormRequest
{

    public function authorize()
    {
      return true;
    }

    public function rules()
    {
         return [
            'title' => sprintf(
              'required|string|unique:posts,title,%s', $this->post->title
            ),
            'description' => 'required|min:8|max:255|string',
        ];
    }
}



  • authorize method is used to determine whether the user is authorized or not make to this request. Thus, it must return boolean.
  • rules method is used to determine the validation rules which we will use later to validate the incoming request against.

We can use it as follows.


<?php

namespace App\Http\Controllers;

use App\Post;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdatePostFormRequest;

class PostController extends Controller
{

    public function edit(Post $post)
    {
        return view('posts.edit')->withPost($post);
    }

    /**
     * @param UpdatePostFormRequest $request
     */
    public function update(Post $post, UpdatePostFormRequest $request)
    {

        $post->update($request->all());
    }
}

Let's hold on for a minute here to figure out what's going on here.

  • we used the edit method to show up the form that will contain a couple of inputs to let the user update that post.
  • we used the update method to update the post.

After understanding what's going on here, Let's understand the update method specifically as it uses the form request.

It's simple, you won't proceed to the body of the update method if there is something went wrong with the validation, as the validation will spit the error messages and redirect you back ( we might not need that in case of API handling because redirecting will cause you a 404 error. We will look into that later).

Note: Don't use $request->all() as this eliminates the benefit of form validation because once you pass the validation stage, you are going to use all of the submitted form data and insert them to the database and these inputs may contain a hidden one or unneeded input that might cause a security issue.

For example, imagine a user used the inspect element or intercepted the request after bypassing the client-side validation, and injected an input field with id value, most probably this will cause the database to squawk if you haven't determined what inputs are fillable or what is guarded.

Instead, you can just use $request->validated(), which will get only the inputs that are passed by the validation, which you only need to insert to the database.

  • Tip: the validated method behind the scenes works by retrieving the data from the request via the keys of the array we have at our rules method at our form request.

     /**
     * Get the attributes and values that were validated.
     *
     * @return array
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    public function validated()
    {
        if ($this->invalid()) {
            throw new ValidationException($this);
        }

        $results = [];

        $missingValue = Str::random(10);

        foreach (array_keys($this->getRules()) as $key) {
            $value = data_get($this->getData(), $key, $missingValue);

            if ($value !== $missingValue) {
                Arr::set($results, $key, $value);
            }
        }

        return $results;
    }


Alright, I think you got the idea of how to play around with form request, Let's get advanced.

What's there for advanced usage?.

  1. how to handle custom messages.
  2. handle authorize dynamically.
  3. handle failed validation and control redirection in case of dealing with API.
  4. handle failed authorization.
  5. how to inject data that must be validated however you don't want the user to submit.
  6. how to customize the passed values after validation.

1. How to handle custom messages

at a certain point while coding, you might need to show a custom message for the user that might be more readable. Fortunately, laravel gives you this advantage by using a method called messages.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostFormRequest extends FormRequest
{

    public function authorize()
    {
      return true;
    }

    public function rules()
    {
      return [
            'title' => sprintf(
              'required|string|unique:posts,title,%s', $this->post->title
            ),
            'description' => 'required|min:8|max:255|string',
        ];
    }

    public function messages()
    {
        return [
            'title.unique' => 'title must be a unique.',
            'description.min'  => 'description minimum length bla bla bla'
        ];
    }
}



You can destruct every single validation rule you created as shown above and write your custom message if needed. Moreover, you can use localization to show message depending on the user's language.

2. handle authorize dynamically.

You probably noticed that we return a hardcoded boolean value at the authorize method, which is useless at the moment. So, how could we take control over that dynamically?. you can do whatever you want using the authenticated user and dig into your roles relationships and by the end of whatever you do, you must return a boolean value. Or, you can just use policy and gates which is something, fortunately, laravel provides by nature.

Have you ever meditated the app/Http/Kernel.php file specifically at the $routeMiddleware array


<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
     /**
     * The application's route middleware.
     *
     * These middlewares may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];
}

We have got the can middleware there which we can use on our routes/controllers. Luckily Laravel gives us the ability to use the functionality of can middleware through our user model if you extend Authenticatable. Here is why.



<?php

namespace App\User;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
}

If you take a look at the Authenticatable which is Illuminate\Foundation\Auth\User.

<?php

namespace Illuminate\Foundation\Auth;

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;

class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword, MustVerifyEmail;
}



Then if you take a look at the Authorizable trait, you will see it there.


<?php

namespace Illuminate\Foundation\Auth\Access;

use Illuminate\Contracts\Auth\Access\Gate;

trait Authorizable
{
    /**
     * Determine if the entity has a given ability.
     *
     * @param  string  $ability
     * @param  array|mixed  $arguments
     * @return bool
     */
    public function can($ability, $arguments = [])
    {
        return app(Gate::class)->forUser($this)->check($ability, $arguments);
    }

    /**
     * Determine if the entity does not have a given ability.
     *
     * @param  string  $ability
     * @param  array|mixed  $arguments
     * @return bool
     */
    public function cant($ability, $arguments = [])
    {
        return ! $this->can($ability, $arguments);
    }

    /**
     * Determine if the entity does not have a given ability.
     *
     * @param  string  $ability
     * @param  array|mixed  $arguments
     * @return bool
     */
    public function cannot($ability, $arguments = [])
    {
        return $this->cant($ability, $arguments);
    }
}

Take a look at the can method, It uses service container to resolve the gate which we talked about earlier. Also, it returns boolean. Which makes you easily can do that

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostFormRequest extends FormRequest
{

    public function authorize()
    {
      return auth()->user()->can('update-post', $this->post);
    }
}


  • Note: In order to use the currently authenticated user, make sure that this route is protected with the auth middleware, in order to avoid crashing. unless that, you are trying to use can method on null.

In order to achieve the above methodology, you have to register a policy and define a gate with the name of 'create-post' and it calls the policy which contains your logic.

  • Laravel also has an artisan command to make you a policy.
php artisan make:policy PostPolicy

  • The convention to name policy is {ModelName} concatenated by Policy.

<?php

namespace App\Policies;

use App\{User, Post};

class PostPolicy
{
    /**
     * Determine if the given post can be updated by the user.
     *
     * @param  \App\User  $user
     * @param  \App\Post  $post
     * @return bool
     */
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}


  • Laravel reserves the first parameter of any policy method and retrieves the user for you. If you have any other model needed you can pass it as other parameters to the method which in our case is the post model.

  • Note: In a real-world example, I keen on using a declarative programming style which at this point can be followed like this.



    public function update(User $user, Post $post)
    {
        return $user->is($post->user);
        // or with extra check if we have roles the declarative way.
        // return $user->is($post->user) && $user->roles->contains($roleModel);

    }

This is way more declarative, as we don't put any logic here that is a burden code and can't be reused, try to avoid that.

  • Note: If you don't what is "is" method, It's used to determine if two models have the same ID and belong to the same table.

Alright, we created our logic for the update method, how to bind this to a gate?.


<?php

namespace App\Providers;

use App\Post;
use App\Policies\PostPolicy;
use Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        Gate::define('update-post', 'App\Policies\PostPolicy@update');


        //
    }
}

Congratulations, now we can achieve what we have done earlier, as we bound the gate name 'update-post' to the logic that's at our policy via using the can method.

3. handle failed validation and control redirection in case of dealing with API.

Laravel by default invokes the method failedValidation whenever any of the rules we created aren't passed. If we dig into this method at the form request that laravel provides we will understand why we get a redirection.

<?php
namespace Illuminate\Foundation\Http;

class FormRequest extends Request implements ValidatesWhenResolved
{

     /**
     * Handle a failed validation attempt.
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function failedValidation(Validator $validator)
    {
        throw (new ValidationException($validator))
                    ->errorBag($this->errorBag)
                    ->redirectTo($this->getRedirectUrl());
    }
}

Uhmmm, we got the error bag there and we got a redirection there which is pretty annoying in case of API requests. How to tackle this? πŸ™„

Let's create our own form request that extends laravel's form request to override this.


<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

abstract class APIRequest extends FormRequest
{
    /**
     * Determine if user authorized to make this request
     * @return bool
     */
    public function authorize()
    {
        return true;
    }
    /**
     * If validator fails return the exception in json form
     * @param Validator $validator
     * @return array
     */
    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response()->json(['errors' => $validator->errors()], 422));
    }
    abstract public function rules();
}

Then at any form request that's used for an API endpoint, we can use this class instead of the form request that laravel provides.

4. handle failed authorization.

whenever there is a failed authorization, laravel throws an exception by default that you can handle at the render method at app/Exceptions/Handler.php


<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * @param  Exception $execption
     * @return void
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        if ($exception instanceof AuthorizationException) {
            return response()->json([
                'message' => $exception->getMessage(),
            ], 401);
        }
        return parent::render($request, $exception);
    }
}

If you don't know what's going on here, whenever an exception is thrown, laravel passes by the render method and then it renders this exception, we can check against the upcoming exception and intercept that and return our own message or whatever you want. unless that let laravel renders.

Alright, but how did we know that the thrown exception is authorization exception?. If we check at the form request class of laravel, we will see that the failed authorization method throws this exception.
we can override this method once more and throw this exception with our own message instead of rendering the message to the end-user "unauthorized attempt" which isn't clear enough at a certain point.


<?php
use Illuminate\Auth\Access\AuthorizationException;

class UpdatePostFormRequest extends FormRequest
{
   public function failedAuthorization()
   {
      throw new AuthorizationException("You don't have the authority to update this post");
   }
}

It's not only tied to throwing this exception with a custom message, feel free to implement any logic needed.

5. how to inject data that must be validated however you don't want the user to submit.

I remember once upon a time I needed that, but I don't remember the situation at the moment. Anyways!, I'll show you how to achieve that, and you might use it at a certain scenario you have.

Laravel takes the request inputs and queries passed and apply the validation rules on them, but it doesn't do that directly, it uses a method called validationData. and at this point, we can override it and inject whatever we want and this method will be called and the validation will be applied on what's returned from the validationData.

Let's take a look at the base form request of laravel, what it does there.


    /**
     * Get data to be validated from the request.
     *
     * @return array
     */
    public function validationData()
    {
        return $this->all();
    }

It returns all of the request data, and it uses the all method because we extend the laravel's request class which has all method.

we can make use of that by overriding this method.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostFormRequest extends FormRequest
{

    public function authorize()
    {
      return true;
    }

    public function rules()
    {
         return [
            'title' => sprintf(
              'required|string|unique:posts,title,%s', $this->post->title
            ),
            'description' => 'required|min:8|max:255|string',
            'user_id' => ''
        ];
    }
    protected function validationData()
    {
        return array_merge($this->all(), [
            'user_id' => $this->user()->id
        ]);
    }
}

We added the user_id to the validation rules, the user won't be passing that for sure, and even if the user passed it, we are going to override it by the user id of the currently authenticated id.

  • Tip: passing a value of empty string for a key means that there's no validation for this key. However, it can exist and we can take it when using validated (since it's a key of the array we have at our rules method)

6. how to customize the passed request values after validation.

I guess you probably guessed it since we have the validated method, we can override it and merge the values coming from the original validated method with whatever we want.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostFormRequest extends FormRequest
{

    public function authorize()
    {
      return true;
    }

    public function rules()
    {
         return [
            'title' => sprintf(
              'required|string|unique:posts,title,%s', $this->post->title
            ),
            'description' => 'required|min:8|max:255|string',
        ];
    }
    public function validated()
    {
        return array_merge(parent::validated(), [
            'user_id' => $this->user()->id
        ]);
    }
}

at this point, the user doesn't pass his user_id, and when we call the validated method, we are calling the validated method we created here which overrides the one that laravel gives us by default. We get the parent validated values and merge them with the user_id, and that way we have the user_id automatically injected and we can use it to directly update/create the record at the database without preparing/associating/attaching it.

Alright, that's it for the form request. I hope you enjoyed it ✌️, I see ya at another article πŸ™ˆ. Don't forget to follow me if you enjoyed this one πŸ‘€

Posted on by:

Discussion

markdown guide
 

Hey Mohammed, thanks for this article. It's very usefuil.
I'm having some issue to handle authorization dynamically in the form request, inside the authorize method.

I have a similar situation where I need to pass a second model in the can method inside the authorize function.

Since I'm using Lumen, I have defined both Gates and Policies but looks like the second model is not being binded in the Form Request.

So my question is: When you do $this->post to get the Post Model, from where this post property is coming from? Are you creating a post property inside the Form Request and binding the model from the Route there? I tried to get the model from the route using $this->route('my_model') without success. I might have missed something.....

Thanks for your time :)

 

Hello Deric, I appreciate that article is beneficial to you 😍
I haven't used Lumen, but there might be something missing, that to access the model instance inside your form request, your route has to be something like that
/posts/{post}
and then you can access it using $this->post or $this->route('post'), bear in mind that naming the route must be same as the name of the property you try to retrieve.

 

Hello Mohammed, I found out what the issue was. Actually it's a bug that's happening with Lumen only during tests.

When you try to access the model with $this->route('model') during tests, a array is being returned instead, which makes the code to break.

Everything is clear now. Thanks for your time.

If you are interested about the issue, more details here: github.com/laravel/lumen-framework...

 

Worse combination:

  1. Save with attributes from $request->all()
  2. Have models' $fillable full with all the attributes (including parameters, that might be used in authorization/roles) or $guarded left empty.
 

when we have file in request, that request variable did not handle it well and we need to use request() to get that file.

 

I haven't faced this problem, I can retrieve the file uploaded through the validated array, as laravel parses the file we uploaded and instantiate a UploadedFile class instance. Also, as mentioned in the article, validated values are being retrieved from the request bag using a similar algorithm to array intersect, or get from the request by keys we already have at our rules.