Foreign key validation rule
In this post I'm going to show you how you can validate foreign keys in your request form objects by calling:
Rule::foreignKey(Subscription::class)
I'm digging it. It's just a simple extension on the exists
rule, but I think it is killer.
Find or fail?
When I started with Laravel, I always found it quick and easy to utilise the Eloquent query builder's findOrFail
method to ensure that any foreign keys I was submitted from a user were valid. Most of the time these foreign keys are coming from a HTML select drop-down in some web form. Of course we want to fail to make sure we don't attempt to insert, into the database, a foreign key that doesn't exist (from some tricksy little hobbits'zs that decide they want to wreak havoc in our apps), but was this the best approach?
Then I got thinking, really the findOrFail
method is just for resources in the URL. That being said - I really don't care what type of error I send someone being malicious.
Say we have a user in the database with an id of 2
. That user has a foreign key of role_id
.
If this user is being updated by calling PATCH: /users/2
, but the request contains a role_id
value that does not exists, it doesn't really make sense that the response would be a 404 error because that user does exist. They should, of course, receive a validation error. But findOrFail
did serve the general purpose for me in the early days.
Validation forms
Keeping all your validation in a form object makes things much easier to maintain. If I'm using a form object and still utilising findOrFail
in the controller for my role_id
foreign key, obviously I haven't validated my data. I should trust that those keys exist, and can just lean on the find
method instead.
Exists rule
Luckily, Laravel offers a handy validation rule exists
to make this easy. As an initial introduction, say we are creating a password reset controller. The request coming in will contain an email address, and we need to check that the email address exists in the database for some user. We might do something like:
$rules = [
'email' => 'required|email|exists:users,email',
];
This will hit the users
table and search through the email
column for the value in the request for email
. If it does not find that email I can present the user with an error letting them know the email address does not exist.
Exists with foreign key, the stringy way
Translating this to a foreign key rule is pretty straightforward. We just need to specify the table and column.
My current project has a User
and Subscription
class. The user has a subscription_id
foreign key field. Let's see how we could validate a submitted subscription_id
value using the exists rule:
$rules = [
'subscription_id' => 'exists:subscriptions,id',
];
Here we are saying, ensure that the request input subscription_id
exists in the subscriptions
table by looking for it in the id
column.
Perfect! Exactly what we were after. But wait - there's more!
Wouldn't it be annoying if we change our table name or primary key column name later on? Then we would have to update these strings manually. But we could always just access those values from an Eloquent instance instead:
$subscription = new Subscription;
$rules = [
'subscription_id' => 'exists:'.$subscription->getTable().','.$subscription->getKeyName(),
];
Hmmm. Although it works, I can't handle this. It's just to messy for me.
Exists with foreign key, the fluent way
Let's utilise Laravel's built in fluent validation rules. These are nice helpers around some of the more complex validation rules to make it easier to do more complex computation within the rules.
use Illuminate\Validation\Rule;
$subscription = new App\Subscription;
$rules = [
'subscription_id' => [
Rule::exists($subscription->getTable(), $subscription->getKeyName()),
]
];
Okay, now we are getting somewhere, but I think we can do better!
Extending the validator
Let's solidify our concept of foreign key validation to make this really clear by extending the Laravel validator, you should do this in a service provider.
We're going to allow you to pass in an Eloquent instance or a class string. It's going to be awesome...
<?php
namespace App\Providers;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Validator::extend('foreign_key', function ($attribute, $value, $parameters, $validator) {
$instance = is_string($parameters[0]) ? new $parameters[0] : $parameters[0];
return new Exists($instance->getTable(), $instance->getKeyName());
});
}
}
Using the foreign key validation rule
We can now access our new rule in our form validation. To kick things off, let's do it the stringy way.
$rules = [
'subscription_id' => 'foreign_key:'.App\Subscription::class,
];
So that will work fine, but again - we can do better! There are a couple of ways we can do this. The first is to extend the Illuminate\Validation\Rule
class and add the method there, or add a macro to it, as it utilises the Macroable
trait, but I'm going to use this as a chance to flaunt the Laravel Validation Rule package I made.
Once you have that installed, just extend the package like so:
TiMacDonald\Validation\Rule::extendWithRules('foreign_key');
You should do this just after you extend the validator in your service provider.
Now we have added all the magic sauce, lets see it in action. When we want to validate that a foreign key exists, we can simply use:
use App\Subscription;
use TiMacDonald\Validation\Rule;
$rules = [
'subscription_id' => Rule::foreignKey(Subscription::class)->get(),
];
or assuming we already had an instance in the $subscription
variable:
use TiMacDonald\Validation\Rule;
$rules = [
'subscription_id' => Rule::foreignKey($subscription)->get(),
];
Update
Laravel 5.5 added custom validation rules, which means the framework now gives you a way to do this really cleanly.