Laravel Nova gives us developers a relatively easy way to set up an administration panel for a website or app, but it’s been publicly available for less than a year and is only at version 1.3.1, so there are a few rough edges, like the inability to use FormRequest
objects for validation when updating models, (called “Resources” in Nova parlance).
FormRequest
objects are a way to detach form validation from specific controllers or routes so that the validation logic can be re-used, for example by using the same FormRequest
object to validate updates made on a website or through an API. But they’re not supported by Nova so validation code needs to be re-written for Nova resources, or does it?
Comparing Laravel to Nova Validation
Validation rules in Nova are the same as the rest of Laravel, with the at least one tiny change in syntax, but instead of being passed around as one big list, each field has validation attached directly to it when defining the field. Here is the difference between rules in a FormRequest
, (it’s the same whether using a FormRequest
, Validator
object, or in a controller), and rules for Nova fields:
Rules using a FormRequest
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StorePostRequest extends FormRequest
{
// ....
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
// note all rules are returned as an array
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
// ....
}
Rules using Laravel Nova
namespace App\Nova;
use Laravel\Nova\Fields\ID;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Textarea;
class Post extends Resource
{
// ...
/**
* Get the fields displayed by the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function fields(Request $request)
{
return [
// ...
// In Nova the rules are attached to fields, which themselves are an array.
Text::make('Title')->rules('required', 'unique:posts', 'max:255'),
Textarea::make('Body')->rules('required')
// ...
];
}
// ...
}
The “Laravel Way” to use a FormRequest
for validation would be to type hint the $request
passed to the fields
method like this, (but it doesn’t work, apparently ):
namespace App\Nova;
use Illuminate\Http\Request;
use App\Http\Requests\StorePostRequest;
class Post extends Resource
{
// ...
// This type-hint produces a fatal error
public function fields(StorePostRequest $request)
{
return [
// ...
];
}
// ...
}
I’m actually not sure why the error is produced. The error I get says that Post@fields
expects an instance of Illuminate\Http\Request
, and that StorePostRequest
isn’t one, but I am pretty sure it is an instance of Illuminate\Http\Request
. Either way it doesn’t work, and that’s the expected behaviour, so a workaround is needed.
A Workaround
One way to only write validation rules once is to move them into a static method on the FormRequest
object that has an optional $rule
parameter to fetch one rule at a time if needed. Since Nova allows separate creationRules
and updateRules
for each field there should be three corresponding static methods on the FormRequest
, and the non-static rules
method can merge the rules for the rest of Laravel if needed.
Here’s the FormRequest
class I’m using to provide validation rules for adding and editing users in an application I’m working on:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Rules\Postalcode;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
// Users authorized to make the request are:
// - users updating themselves.
// - staff
// - guests creating a new user.
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = self::ruleGetter($this);
if(empty($this->user())) {
$rules = array_merge_recursive($rules, self::creationRuleGetter($this));
} else {
$rules = array_merge_recursive($rules, self::updateRuleGetter($this));
}
return $rules;
}
public static function ruleGetter( $request, $rule = null ) {
$rules = [
'email' => [
'required',
'string',
'email',
'max:255'
],
'nickname' => [
'required',
'max:255'
],
'job_title' => [
'sometimes',
'max:255'
],
'organization' => [
'sometimes',
'max:255'
],
'state' => [
'sometimes',
'nullable',
'in:AL,IS,TO,FS,TA,TE,S'
],
'postalcode' => [
'sometimes',
new Postalcode
],
'phone' => [
'max:255'
]
];
if(!empty($rule)) {
if(isset($rules[$rule])) {
return $rules[$rule];
}
return '';
}
return $rules;
}
public static function creationRuleGetter( $request, $rule = null ) {
$rules = [
'email' => [
'unique:users,email'
],
'nickname' => [
'required',
'unique:users,nickname'
],
'password' => [
'sometimes',
'required',
'string',
'min:8',
'confirmed'
],
];
if(!empty($rule)) {
if(isset($rules[$rule])) {
return $rules[$rule];
}
return '';
}
return $rules;
}
public static function updateRuleGetter( $request, $rule = null ) {
$rules = [
'email' => [
Rule::unique('users', 'email')->ignore($request->user()->id)
],
'nickname' => [
Rule::unique('users', 'nickname')->ignore($request->user()->id)
],
'password' => [
'sometimes',
'nullable',
'string',
'min:8',
'confirmed'
],
];
if(!empty($rule)) {
if(isset($rules[$rule])) {
return $rules[$rule];
}
return '';
}
return $rules;
}
}
Then, in the Nova resource the rules can be attached to individual fields like this:
<?php
namespace App\Nova;
use Laravel\Nova\Fields\ID;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Gravatar;
use Laravel\Nova\Fields\Select;
use Laravel\Nova\Fields\Password;
use Laravel\Nova\Panel;
use Laravel\Nova\Fields\DateTime;
use Laravel\Nova\Fields\Textarea;
use Laravel\Nova\Fields\Place;
use Laravel\Nova\Http\Requests\NovaRequest;
use App\Http\Requests\StoreUserRequest;
class User extends Resource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static $model = 'App\\User';
// ....
/**
* Get the fields displayed by the resource.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Gravatar::make(),
Text::make('Email')
->sortable()
->withMeta( [
'extraAttributes' => [
'type' => 'email'
]
])
->rules(StoreUserRequest::ruleGetter($request, 'email'))
->creationRules(StoreUserRequest::creationRuleGetter($request, 'email'))
->updateRules('unique:users,email,{{resourceId}}'),
Password::make('Password')
->onlyOnForms()
->rules(StoreUserRequest::ruleGetter($request, 'password')),
Text::make('Name', 'name')
->sortable()
->rules(StoreUserRequest::ruleGetter($request, 'name')),
Text::make('Preferred Name')
->rules(StoreUserRequest::ruleGetter($request, 'preferred_name'))
->hideFromIndex(),
Text::make('Nickname')
->rules(StoreUserRequest::ruleGetter($request, 'nickname'))
->creationRules(StoreUserRequest::creationRuleGetter($request, 'nickname'))
->updateRules('unique:users,nickname,{{resourceId}}')
->hideFromIndex(),
Text::make('Job Title')
->rules(StoreUserRequest::ruleGetter($request, 'job_title'))
->hideFromIndex(),
Text::make('Company Name', 'organization')
->rules(StoreUserRequest::ruleGetter($request, 'organization'))
->hideFromIndex(),
Place::make('Address')
->rules(StoreUserRequest::ruleGetter($request, 'address'))
->countries(['US', 'CA'])
->secondAddressLine('address2')
->city('city')
->state(['stateprov'])
->postalCode('postalcode')
->hideFromIndex(),
Text::make('Address Line 2', 'address2')
->rules(StoreUserRequest::ruleGetter($request, 'address2'))
->hideFromIndex(),
Text::make('City')
->rules(StoreUserRequest::ruleGetter($request, 'city'))
->hideFromIndex(),
Select::make('State or Province', 'stateprov')
->options(get_stateprovs())
->rules(StoreUserRequest::ruleGetter($request, 'stateprov'))
->hideFromIndex(),
Text::make('ZIP or Postal Code', 'postalcode')
->rules(StoreUserRequest::ruleGetter($request, 'postalcode'))
->hideFromIndex(),
Text::make('Phone')
->rules(StoreUserRequest::ruleGetter($request, 'phone'))
->withMeta(['extraAttributes' => [
'type' => 'tel'
]])
->hideFromIndex(),
];
}
// ....
}
Instead of writing out actual strings of rules for the Nova resource, the StoreUserRequest::ruleGetter
method supplies the rule, allowing one canonical repository of rules for use with Users.
An Exception
There is an exception to the beauty: Enforcing unique values in a table, but ignoring a certain user ID, (for example when updating a user profile the user should not see a “This E-mail is already in use” error if the E-mail isn’t changed). Nova handles it like this:
// Simple and elegant
Text::make('Email')
->rules('unique:users,email,{{resourceId}}')
Non-Nova Laravel handles it like this:
Rule::unique('users')->ignore($user->id, 'user_id')
But when declaring Nova fields a $user object
, or the ID of the model that is being updated, is not available so this does not work. I have worked around the problem by writing out these few cases in the updateRules()
method of the Nova field, as in this excerpt from above:
// Excerpt from full Nova Resource definition.
// Most Rules use one of the ruleGetter methods on StoreUserRequest
// but updateRules are just typed out to take advantage of Nova syntax.
Text::make('Email')
->rules(StoreUserRequest::ruleGetter($request, 'email'))
->creationRules(StoreUserRequest::creationRuleGetter($request, 'email'))
->updateRules('unique:users,email,{{resourceId}}'),
Other Possibilities
Ideally it would be possible to have the FormRequest
object do the validation itself, without having to pass individual rules to Nova fields. Another possibility is to turn the Request
that’s passed to a Resource’s fields method into a FormRequest
, or to instantiate a completely new FormRequest
object, and let the FormRequest
object handle validation. Unfortunately I don’t know where we would do this, (the fields method doesn’t seem right), and I don’t know if the redirects and error messages generated by FormRequest
validation would work properly with Nova. I would love to hear if someone can make this work.
Conclusion
I sincerely hope that Nova will soon allow automatic validation with FormRequest
objects, but until that happens it is possible to keep code almost DRY and feed individual rules to Nova fields with ruleGetter
methods on our FormRequests
.
This really helped me where I had been stuck with this issue for complex file upload validation rules in Nova for nearly a week – thanks so much!
I’ve been dealing with some of the same frustration with Nova’s inability to (re-)use form requests. My solution can be found here: https://github.com/telkins/laravel-validation-rulesets
If you have thoughts, please share them. I’m not sure it’s a perfect solution for these kinds of problems, but it’s working out for me…so far. 😉
That’s pretty cool Travis. Thanks for sharing.