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.

WP e-Commerce to WooCommerce

There’s a pair of websites that I have looked after for a long time. They belong to a company that sells a few products, and have had quite a few sales over the years. This fall we’re doing some major renovations, and one of the things being changed is the underlying E-Commerce plugin for WordPress; we are changing from WP e-Commerce to WooCommerce. I expected that after installing WooCommerce it would offer to convert my WPeC store into a WooCommerce store. I was wrong. There was an official converter plugin but it was abandoned long ago.

Options to change a site from WP e-Commerce to WooCommerce

It looks like there are three ways to change a site from WP e-Commerce to WooCommerce:

  1. Use a commercial service like Cart2Cart. Cart2Cart looks great, but is made to move a store from one site to another, and I need an in-place conversion. They also charge per-migration, so if I want to migrate my localhost, modify the theme for WooCommerce, then migrate the live site, I’ll have to pay 1.5 times, (they give a 50% discount for re-migrating a site). With two sites my bill would have been around US$450, high enough for me to look at other, more complicated options.
  2. Create or update something like the old WooCommerce migration plugin.
  3. Manual Migration: This would involve a lot of SQL.

Cart2Cart looks ill-suited for my use-case, and expensive. Manual migration looks difficult, highly prone to errors, and not easily repeatable. This leaves creating or updating a migration utility. This migration script by Carl Hughes is bit newer than the WooCommerce one, promises to do more out of the box, and quick read-through doesn’t show anything too mysterious. It was my starting point.

Updating and Testing

It took three times as long as expected to update & test the migration plugin. Here’s what I learned, for anyone else that might be considering the same type of process:

  • This should go without saying, but this migration is destructive. If it fails the database will be in a strange state between WPeC and WooCommerce. Have a backup.
  • Since migrating from WPeC to WooCommerce changes the database, but only the database, (no media files are touched), a backup of the WordPress database is required. A lot of time will be spent reloading from the backup, so put it somewhere easy to get. Mine was on my desktop. Bonus points: a short bash script to reload the DB from the backup will save development time.
  • Make the backup be the exact state that the migration should run from. It is not efficient to disable a plugin in the WP Admin every time the database is reloaded.
  • Some plugins really slow down the migration. The most obvious ones are WP e-Commerce itself, and any Varnish caching plugin. If they are disabled the migration will run faster.
  • Each payment gateway, (plugin for a payment processor, in WPeC terminology), stores transaction data a little differently. I added the ability to port data from Authorize.Net credit card transactions, but separate routines need to be written for other processors.

Updated Migration Plugin

My fork of the migration plugin is up on Github, and a I sent a pull request to the original author so he can include my work if he wants. If you need to migrate from WP e-Commerce to WooCommerce hopefully this can help a bit. Read the readme. Migrating is harder than it should be, but it is possible.

Changing the Name and Path of the Active WordPress Theme Without Breaking Theme Settings

After changing a brand name, but little else, on a WordPress site we can end up with a WordPress theme called “oldbrand” on a site called “New Brand.” This leads to weird things like the brand’s logo being at the URL newbrand.com/wp-content/themes/oldbrand/img/newbrand-logo.png. Oops. SEOs must cry a little when they see something like that. This is a situation one of my clients ended up in after a name change, but it ends now.

We can’t simply change the theme directory from themes/oldbrand to themes/newbrand and update the active theme in the Appearance section of the wp-admin because theme-related settings are tied to the theme directory in the database. This includes things like widget placement; a different theme probably has very different widget areas so widget placement should be stored per-theme. The same goes for any theme settings & customizations. To get around this we need to update the database so WordPress thinks that the new theme name is the theme name we’ve always been using.

A word of caution: back up your database before trying this. We’re going to run raw queries. If something breaks you may end up in an in-between limbo, and restoring from backup is the quickest way out.

After diving through the database of a few sites this morning these queries make it possible to rename the theme directory, and optionally rename the theme in style.css:

# Update the main theme options
UPDATE wp_options SET option_value='new-theme-directory' WHERE option_name='template';
UPDATE wp_options SET option_value='new-theme-directory' WHERE option_name='stylesheet';
UPDATE wp_options SET option_name='theme_mods_new-theme-directory' WHERE option_name='theme_mods_old-theme-directory';

# If also updating the Theme Name in the theme's style.css
UPDATE wp_options SET option_value='New Theme Name' WHERE option_name='current_theme';  

# If any posts reference assets in the theme such as images or logos.
UPDATE wp_posts SET post_content=REPLACE(post_content, 'themes/old-theme-directory', 'themes/new-theme-directory' );
UPDATE wp_postmeta SET meta_value=REPLACE(meta_value, 'themes/old-theme-directory', 'themes/new-theme-directory');

# If WordFence is used
UPDATE wp_wfConfig SET val=REPLACE(val, 'themes/old-theme-directory', 'themes/new-theme-directory');

# Delete a couple of transients that store references to the old theme directory.
DELETE FROM wp_options WHERE option_value='_site_transient_theme_roots';
DELETE FROM wp_options WHERE option_value='_site_transient_update_themes';

Protips:

  • Try to run these queries at nearly the same time as the theme directory is renamed. Bonus points for making a script that does it all nearly instantly.
  • If an installation stores transients somewhere other than the DB, then clearing them in the DB won’t work, (clearing the transients may not be strictly required).
  • The sample code uses the default wp_ table prefix. When working on a site with a different prefix, use that.

Hopefully this helps someone finish their Googling session and get on with a rename!

Preview Laravel Password Reset and Verification E-mails in a Browser

I’m building something on Laravel, using the built-in Auth with 5.7’s new E-mail Verification feature. Today’s task was to tweak the design of E-mail notifications a bit, so I wanted to preview them in a browser. I thought it would be easy, but the built-in E-mails use the Notification system, bypassing Mailable objects. Some Googling didn’t help much, but combining the Notifications documentation and this forum post got let me preview the messages. Hopefully this post can help someone else preview their messages much more quickly than I did today.

The concept is similar to the “Previewing Mailables” documentation: define a route to view your preview on, and return something that will render the message. The trick is that the MailMessage objects that the Notifications’ toMail() methods return don’t have a render() method like Mailable class does, so we need to find something else. Since the built-in Auth uses Markdown mail we can use the Markdown class. A Notifiable object, (the User object for the user who will get the notification), and a Notification are also needed.

For this to work you need to have the right Notifications published to your resources/views/vendor directory. This is done with the Artisan command php artisan vendor:publish --tag=laravel-notifications.

And set up the preview routes:


Route::get('/reset-notification', function () {

    // Get a user for demo purposes
    $user = App\User::find(1);

    // Make a Reset Notification object, (subclass of `Notification`)
    $resetNotification = new \Illuminate\Auth\Notifications\ResetPassword( 'some-random-string-this-will-be-much-longer-in-real-life' );

    // get the `MailMessage` object
    $mailMessage = $resetNotification->toMail($user);

    // get the `Markdown` object
    $markdown = new Illuminate\Mail\Markdown(view(), config('mail.markdown'));

    // Render the vendor.notifications.email view with data from the `MailMessage` object
    return $markdown->render('vendor.notifications.email', $mailMessage->toArray());
});


Route::get( '/verify-notification', function () {

    // Get a user for demo purposes
    $user = App\User::find(1);

    // Make a Verify Notification object, (subclass of `Notification`)
    $verifyNotification = new Illuminate\Auth\Notifications\VerifyEmail();

    // get the `MailMessage` object
    $mailMessage = $verifyNotification->toMail($user);

    // get the `Markdown` object
    $markdown = new Illuminate\Mail\Markdown(view(), config('mail.markdown'));

    // Render the vendor.notifications.email view with data from the `MailMessage` object
    return $markdown->render('vendor.notifications.email', $mailMessage->toArray());
});

Once the Notification is published and the routes created it is possible to preview the Password Reset and E-mail Verification E-mails in the browser.

Automating the Fracturing of a Git Repository

I have a git repo that’s become a monster. It’s got at least two WordPress themes, a handful of custom WordPress plugins, some .htaccess files, an artwork directory, and more. Back in the dark times, when it was a Subversion repository this sort of made sense, in that I didn’t have to set up a new repo on the server for each component in the project. But I haven’t touched SVN in a long time, and making new repositories in Git is easy. We’re doing some major work on this client’s sites this fall, so it’s time to break up the giant repository into several smaller ones.

The monolith repository is being split into seventeen smaller components. One of these components is an “artwork” directory, the ~600MB history of which will bloat the git history of all sixteen other repositories if we don’t purge it properly. The goals of this script are:

  1. Avoid typing the same set of commands seventeen times.
  2. Keep the appropriate git history for each of the seventeen components.
  3. Purge any unrelated git history so each component only has its own history in its .git directory.

Github has a help page on splitting a subfolder into its own repository, and another on removing files from a repository’s history, (it’s meant for sensitive data, but works for all data). In theory we only really need the first link, but in testing the entire history of the monolith remained when only following the instructions in the first link, but with the purge and garbage collection commands from the second link the git history is down to an appropriate size.

The script is relatively simple, but relies a bit on directory structure. It should be in the same directory as the monolith repo, (the script doesn’t go into the monolith, it goes in the same containing folder). There are two places to change the script:

  1. Put the directories for extraction into the repos array, (starting on line 5). These paths are relative to the root of the repository.
  2. Put the path of a copy of the monolith repo for use as a source in the master variable, (line 16).

Once that’s done, running ./export-directory-repos.sh should export the repositories.

Here’s the gist:


#!/bin/bash
# Enter the paths in the main repo to the subdirectories you want to extract
# Separate paths with spaces or newlines
repos=(
artwork
checklists
code/comingsoon
code/utility
)
echo "Total repos to export : ${#repos[*]}"
# Put the directory of the main repo here.
master=monolith-repo-master
for repo in "${repos[@]}"
do
subdir=$(basename $repo)
echo "${repo} => ${subdir}"
cp -R $master $subdir
cd $subdir
git filter-branch –prune-empty –subdirectory-filter $repo master
git for-each-ref –format='delete %(refname)' refs/original | git update-ref –stdin
git reflog expire –expire=now –all
git gc –prune=now
cd ../
done