FormRequest object Validation for Laravel Nova Resources

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.

Generating Form Elements with Javascript in IE 10

There’s a DOM manipulation gotcha in IE10 that just got me, and Google didn’t help much, so I’m giving Google something to show anyone who has this problem in the future.

When you dynamically generate a form input in Internet Explorer 10 the you must set the type attribute before doing anything else to the element, otherwise any values you set will be ignored when submitting the form, and in some cases will not be displayed. When inspecting elements using IE’s developer tools, however, the correct value appears in the generated document tree.

So this works as expected:

var sub = document.createElement('input');
sub.setAttribute('type', 'submit');
sub.setAttribute('value', 'Submit Generated Button');


But this doesn't:

var sub = document.createElement('input');
sub.setAttribute('value', 'Submit Generated Button');
sub.setAttribute('type', 'submit');

This is particularly hard to catch with radio buttons and checkboxes, (this is what got me). Their default value is “on” which doesn’t tell me much, especially if you’re submitting an array of them.

Here’s a demo. The first button shows its value, (and submits its value), and the second shows, (and submits) the default of “Submit Query.”

The only documentation I found on this behaviour is a passing sentence in the createElement documentation at MSDN. When they say “then set the type property to the appropriate value in the next line of code” they’re serious about the next line of code.

Also, who chooses “Submit Query” as a default value for a submit button in 2013 anyway? Are they trying to confuse people? Shouldn’t it just be “Submit”?