Laravel: How to self-validate model before save?

Is there any way to keep model validation rules in a model class? Maybe we can auto-validate a Model before it is saved/updated anywhere in the code without writing model-related validation logic in controllers? Yes, it is possible. And in this article, I am reviewing the case.

Table of Contents

Validate model using package

There are several packages on the web to validate the model, but most of them are not working with newer Laravel versions. That’s why I created a simple model class as a package for this purpose which is called minuteoflaravel/laravel-self-validating-model. To install run this command in the terminal:

composer require minuteoflaravel/laravel-self-validating-model

Then your model class should extend MinuteOfLaravel\Validation\SelfValidatingModel instead of extending Illuminate\Database\Eloquent\Model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use MinuteOfLaravel\Validation\SelfValidatingModel as Model;

class Contact extends Model
{
    use HasFactory;
}

And finally, add public $rules property to your model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use MinuteOfLaravel\Validation\SelfValidatingModel as Model;

class Contact extends Model
{
    use HasFactory;
    
    public $rules = [
        'first_name' => 'required',
        'last_name' => 'required',
        'email' => 'required|email:rfc,dns,spoof',
    ];
}

That’s it. Usage is as easy as the class itself. You can even do it without this package. Let’s look at the self-validating model class added by a package:

<?php

namespace MinuteOfLaravel\Validation;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Validator;

class SelfValidatingModel extends Model {
    public $rules = [];

    public static function boot()
    {
        parent::boot();

        static::saving(function ($model) {
            Validator::make($model->toArray(), $model->rules)->validate();
        });
    }
}

As you can see, here are just the $rules property and boot() method added. The boot() method registers a callback for the Eloquent saving event. The callback simply validates the model against the $rules property. If you don’t want to use the package – add this class to your project and extend it.

As an alternative, you can use another package called theriddleofenigma/laravel-model-validation. The highest Laravel version this package supports is Laravel 8. To install the package run this composer command:

composer require theriddleofenigma/laravel-model-validation

Here is an example of usage:

use Enigma\ValidatorTrait;

class User extends Model
{
    use ValidatorTrait;

    /**
     * Boot method.
     */
    public static function boot()
    {
        parent::boot();

        // Add this method for validating the current model on model saving event
        static::validateOnSaving();
    }

    public $validationRules = [
        'name' => 'required|max:10',
        'email' => 'required|email',
    ];

    public $validationMessages = [
        'name.required' => 'Name field is required.',
        'email.email' => 'The given email is in invalid format.',
    ];

    public $validationAttributes = [
        'name' => 'User Name'
    ];

    /**
     * Code to be executed before the validation goes here.
     */
    public function beforeValidation()
    {
        // Some code goes here..
    }

    /**
     * Code to be executed after the validation goes here.
     */
    public function afterValidation()
    {
        // Some code goes here..
    }
}

As you see, your model should use Enigma\ValidatorTrait, call static::validateOnSaving() in static boot() method, add $validationRules,$validationMessages and $validationAttributes messages. Also you can run some code before and after validation using beforeValidation() and afterValidation() methods.

Validate model using Eloquent events

As you have already noticed, packages are using Eloquent events to auto-validate the model. But there is a bit different way of how to use Eloquent events for this purpose. Perhaps, some developers will prefer this way.

Let’s start with adding model validation rules to the model’s public static $rules property:

public static $rules = [
    'first_name' => 'required',
    'last_name' => 'required',
    'email' => 'required|email:rfc,dns,spoof',
];

Next, create validate() method in the model:

public function validate() {
    Validator::make($this->toArray(), $this->rules)->validate();
}

Now, you should create a Laravel event and listener. The event will be dispatched before our model will be saved. For example, let’s create a ContactSaving event and a ValidateContact listener. In the app/Providers/EventServiceProvider.php add event listener mappings:

protected $listen = [
    'App\Events\ContactSaving' => [
        'App\Listeners\ValidateContact',
    ],
];

Run the Artisan command to create these files:

php artisan event:generate

Eloquent events pass the model instance to the Laravel event so you should add it to the event constructor to call the model’s validate() method in the listener. Open app\Events\ContactSaving.php and add this code:

public function __construct(Contact $contact)
{
    $this->contact = $contact;
}

Now open the listener file app\Listeners\ValidateContact.php and call the model’s validate() method:

public function handle(ContactSaving $event)
{
    $event->contact->validate();
}

The last step is to dispatch our ContactSaving event as an Eloquent saving event callback. It is done by adding the $dispatchesEvents property in our model class:

protected $dispatchesEvents = [
    'saving' => ContactSaving::class,
];

Move validation rules to the model

In case you don’t want to self-validate the model but want to move validation rules to the model, you can do this by adding public static property $rules to your model and passing it to the $request->validate() method. Here is an example:

public static $rules = [
    'first_name' => 'required',
    'last_name' => 'required',
    'email' => 'required|email:rfc,dns,spoof',
];

public function store(Request $request)
{
    $request->validate(Contact::$rules);
    Contact::create($request->all());

    return back();
}

If you prefer using form requests, you can use $rules with form requests like here:

 public static $rules = [
    'first_name' => 'required',
    'last_name' => 'required',
    'email' => 'required|email:rfc,dns,spoof',
];
public function rules()
{
    return Contact::$rules;
}
public function store(ContactRequest $request)
{
    Contact::create($request->all());

    return back();
}