Currently I'm working on a project with a lot of forms that have repeated form fields. For example, pretty much all entities in the system can have attachments, so a lot of forms contain the same attachment fields. I could repeat the same validation rules in all form request classes, but this quickly breaks down. What if the rules for attachments change? Let's see how we can handle this more efficiently.
Using Helper Methods
The first approach that comes to mind is using protected helper methods in our base class. Laravel comes with an abstract Request
class out of the box that all form requests extend, so this would be a great place to put that.
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
abstract class Request extends FormRequest
{
protected function attachmentRules()
{
return [
'attachment.*.file' => 'required|file|min:1',
'attachment.*.name' => 'required|string',
];
}
}
Then when we have a form that has attachment fields, we can simply the helper in our rules
method:
<?php
namespace App\Http\Requests;
class SaveInvoiceRequest extends Request
{
// ...
public function rules()
{
return array_merge([
// Invoice rules..
], $this->attachmentRules());
}
}
While this approach works fine, there are two things I don't like about it.
- Our
Request
class becomes littered with all sorts of methods that return validation rules. - We have to use
array_merge
in ourrules
method every time we want to reuse the validation rules, which isn't very clean.
Let's see how we can fix these two issues!
Extracting a Trait
The solution to the first issue is to extract our attachmentRules
method to a trait. This gives us a dedicated place to manage any validation rules related to the attachment form partial.
<?php
namespace App\Http\Requests;
trait HasAttachmentFields
{
protected function attachmentRules()
{
return [
'attachment.*.file' => 'required|file|min:1',
'attachment.*.name' => 'required|string',
];
}
}
Now we can make use of this trait in our form request classes by use
ing it at the top.
<?php
namespace App\Http\Requests;
class SaveInvoiceRequest extends Request
{
use HasAttachmentFields;
// ...
public function rules()
{
return array_merge([
// Invoice rules...
], $this->attachmentRules());
}
}
The second issue is a little harder to tackle. How can we extend our rules method without using array_merge
all the time?
Automatically Adding the Validation Rules
The solution I came up with is to automatically add the validation rules when setting up our validator. To do this we can use a hook that Laravel provides to form request classes called withValidator
. This method gets called by Laravel after constructing the Validator
instance, which is passed as an argument. This allows us to do any further processing on the Validator
instance, like adding more rules.
For this to work efficiently we are going to use a convention, namely Has*Fields
, where the *
is the name of form partial (in this case 'Attachment'). The rules method name then is the camelCase version of this name, followed by Rules (in this case 'attachmentRules'). This means that if we have another trait named HasUserFields
, the rules method in this trait should be named userRules
.
Let's look at the implementation:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
abstract class Request extends FormRequest
{
public function withValidator(Validator $validator)
{
if ($rules = $this->getTraitRules()) {
$validator->addRules($rules);
}
}
protected function getTraitRules()
{
return array_reduce(class_uses(static::class), function ($rules, $trait) {
$rulesMethod = $this->makeRulesMethodName($trait);
if ($rulesMethod && method_exists($this, $rulesMethod)) {
// If the trait name matches our convention and a <name>Rules
// method exists, merge the result into our rules array.
$rules = array_merge($rules, $this->{$rulesMethod}());
}
return $rules;
}, []);
}
protected function makeRulesMethodName($trait)
{
preg_match('/^Has([A-Za-z]+)Fields$/', class_basename($trait), $matches);
// If the trait matches our `Has<name>Fields` convention, get the
// <name> part, camelCase it and attach 'Rules' at the end.
return isset($matches[1]) ? camel_case($matches[1]).'Rules' : null;
}
}
I've commented the code to make it easier to follow. Basically what happens is that we loop through all traits used by the class and if it matches our convention of Has<name>Fields
, we call the <name>Rules
method and merge that into the result. If the result is not empty, we call the addRules
method on the Validator to add the trait rules.
Now we can get rid of our array_merge
call!
<?php
namespace App\Http\Requests;
class SaveInvoiceRequest extends Request
{
use HasAttachmentFields;
// ...
public function rules()
{
return [
// Invoice rules...
];
}
}
Conclusion
Using this system, we can now easily add reusable validation rules to our form request classes! All we need to do is create a new trait with a rules method that matches our convention, use it in the form request and we're good to go! The final code of the Request class is available as a GitHub Gist. Let me know what you think of this approach!