Using $fillable for validation

The "fillable" property on your Eloquent models might just be a sensible place to put your validation logic. After all, given how this list is practically part of validation, it would be a waste not to use that same structure for defining the rules respectively.

Laravel2 / 3Level:

When you think about the $fillable property, you may come to the conclusion that this is actually related to validation: we're telling Laravel which attributes are allowed to be filled and this, at least in my opinion, correlates to our validation rules, that determine how these attributes are allowed to be filled.

With that being said, why not consider merging the two, so our $fillable property includes the validation rules as its values, while the attributes are used as keys:

use Illuminate\Database\Eloquent\Model;

class Post extends Model {

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'title' => 'required|string|min:3|max:20',
        'content' => 'nullable|string|max:2000',
        'published' => 'required|boolean',
    ];
}

Before this actually works, we need to add the following to our Model base class:

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

class Model extends BaseModel
{
    /**
     * Get the fillable attributes for the model.
     *
     * @return array<string>
     */
    public function getFillable()
    {
        return array_is_list($this->fillable) 
            ? $this->fillable
            : array_keys($this->fillable);
    }

    /**
     * Validate the data using model's fillable rules.
     * 
     * @source https://marknuyens.nl/tips/using-fillable-for-validation
     *
     * @param  array  $data  The input data.
     * @param  array  $rules  Any custom rules.
     * @param  array  $messages  Any custom messages.
     * @param  array  $attributes  Any custom attributes.
     * @return self
     * @throws \Illuminate\Validation\ValidationException
     */
    public function validate(
        array $data,
        array $rules = [],
        array $messages = [],
        array $attributes = [],
    ) {
        if(! array_is_list($this->fillable)) {
            $rules = array_merge($this->fillable, $rules);
            Validator::make($data, $rules, $messages, $attributes)->validate();
        }

        return $this->fill($data);
    }
}
💡 By the way, if you're planning to use anything other than strings for defining your rules, you could use the getFillable method instead. That way, you can return things like the Rule class.

Now with that in place, we should be able to use our new trick wherever we want, for instance, inside of our controller:

class PostController
{
   /**
     * Store a newly created resource in storage.
     *
     * @param  Illuminate\Http\Request  $request
     * @param  \App\Models\Post  $post
     * @return int
     */
    public function store(Request $request, Post $post)
    {
        return $post->validate($request->input())->create();
    }
}
💭 Although this might qualify for a small PHP package, just showing the code is probably easier.

Remarks

In the past, I've always used custom FormRequest classes to follow Laravel's best practices. However, this often lead to having to manage multiple files and forgetting or mistyping attributes names. That being said, if you're working with a large team and want to minimize any confusion, I would recommend maintaining those existing validation flows.

There are other ways available for altering your validation flow, one of which is Spatie's Data package, which includes validation (among other things). However, I found this package to come with too many options.

Conclusion

I think this method could prove to be elegant if you prefer to keep your classes DRY and to a bare minimum, while still having the freedom to append this custom validation method whenever it meets your needs.

Although some may argue how you "shouldn't fight the framework", I think that mostly applies to changing or overriding certain behavior Laravel does automagically. However, a change as small as this one might outweigh the drawbacks.

🎁 Bonus

If you want to take this a step further, you could consider applying the trait below to your base model. This will validate your model's input values before they are saved to the database, saving you from having to call validate() manually.

However, the obscurity of this approach may make it hard to read and debug. I would consider this the most efficient method, but perhaps not the most readable. It does, however, keep things very clean. 😏

trait FillableValidation {
    /**
     * Boot the trait.
     *
     * @return void
     */
    protected static function bootFillableValidation()
    {
        static::saving(fn ($model) => $model->validate());
    }
}

Thanks for reading and hope to see you around!