Andrew Welch · Insights · #craftcms #php #behaviors

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Extending Craft CMS with Validation Rules and Behaviors

Craft CMS is a web appli­ca­tion that is amaz­ing­ly flex­i­ble & cus­tomiz­able using the built-in func­tion­al­i­ty that the plat­form offers. Use the platform!

Craft CMS is built on the rock-sol­id Yii2 frame­work, which is some­thing you nor­mal­ly don’t need to think about. It just works, as it should.

But there are times that you need or want to extend the plat­form into some­thing tru­ly cus­tom, which we looked at in the Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule article.

In this arti­cle, we’ll talk about two ways you can use the plat­form that you nor­mal­ly don’t even have to think about to your advantage.

Use the Platform

Some­thing we com­mon­ly hear in fron­tend devel­op­ment is to use the plat­form”, which I think is fan­tas­tic advice. Why re-invent an elab­o­rate cus­tom set­up when the plat­form already pro­vides you with a bat­tle-worn way to accom­plish your goals?

The same holds true with any kind of devel­op­ment. If you’ve writ­ing some­thing on top of a plat­form — what­ev­er that plat­form may be — I think it always makes sense to try to lever­age it as much as possible.

It’s there. It’s well thought-out. It’s test­ed. Use it!

When we’re using Craft CMS, we’re also using the Yii2 plat­form that it’s built on. Indeed, as we dis­cussed in the So You Wan­na Make a Craft 3 Plu­g­in? arti­cle, to know Craft plu­g­in devel­op­ment, you will want to learn some part of Yii2.

So let’s do just that! The Yii2 doc­u­men­ta­tion is a great place to start.

Link Models & Rules

Mod­els are a core build­ing block of Yii2, and so also Craft CMS. They are at the core of the Mod­el-View-Con­troller (MVC) par­a­digm that many frame­works use.

Models are objects representing business data, rules and logic.

Mod­els are used to rep­re­sent data and val­i­date data via a set of rules. For instance, Craft CMS has a User ele­ment (which is also a mod­el) that encap­su­lates all of the data need­ed to rep­re­sent a User in Craft CMS.

It also has val­i­da­tion rules for the data:

/**
 * @inheritdoc
 */
protected function defineRules(): array
{
    $rules = parent::defineRules();
    $rules[] = [['lastLoginDate', 'lastInvalidLoginDate', 'lockoutDate', 'lastPasswordChangeDate', 'verificationCodeIssuedDate'], DateTimeValidator::class];
    $rules[] = [['invalidLoginCount', 'photoId'], 'number', 'integerOnly' => true];
    $rules[] = [['username', 'email', 'unverifiedEmail', 'firstName', 'lastName'], 'trim', 'skipOnEmpty' => true];
    $rules[] = [['email', 'unverifiedEmail'], 'email'];
    $rules[] = [['email', 'password', 'unverifiedEmail'], 'string', 'max' => 255];
    $rules[] = [['username', 'firstName', 'lastName', 'verificationCode'], 'string', 'max' => 100];
    $rules[] = [['username', 'email'], 'required'];
    $rules[] = [['username'], UsernameValidator::class];
    $rules[] = [['lastLoginAttemptIp'], 'string', 'max' => 45];
// ...
}

If this looks more like con­fig than code to you, then you’d be right! Mod­el val­i­da­tion rules are essen­tial­ly a list of rules that the data must pass in order to be con­sid­ered valid.

Check out the Yii2 Val­i­dat­ing Input arti­cle for a real­ly good overview of val­ida­tors, and how they can be used.

Yii2 has a base Val­ida­tor class to help you write val­ida­tors, and ships with a whole bunch of use­ful Core Val­ida­tors built-in that you can leverage.

And we can see here that Craft CMS is doing just that in its craft\elements\User.php class. Any val­i­da­tion rule is an array:

  1. Field — the mod­el field (aka attribute or object prop­er­ty) or array of mod­el fields to apply this val­i­da­tion rule to
  2. Val­ida­tor — the val­ida­tor to use, which can be a Val­ida­tor class, an alias to a val­ida­tor class, PHP Callable, or even an anony­mous func­tion for inline val­i­da­tion
  3. [params] — depend­ing on the val­ida­tor, there may be addi­tion­al option­al para­me­ters you can define

So in the above User Ele­ment exam­ple, the email & unverifiedEmail fields are using the built-in email core val­ida­tor that Yii2 provides.

The username has sev­er­al val­i­da­tion rules list­ed, which are applied in order:

  1. string — This val­ida­tor checks if the input val­ue is a valid string with cer­tain length (100 in this case)
  2. required — This val­ida­tor checks if the input val­ue is pro­vid­ed and not empty
  3. User­nameVal­ida­tor — This is a cus­tom val­ida­tor that P&T wrote to han­dle val­i­dat­ing the user­name field

The user­name field actu­al­ly gives us a fun lit­tle tan­gent we can go on, so let’s peek under the hood to see how sim­ple it can be to write a cus­tom validator.

Here’s what the class looks like:

<?php
/**
 * @link https://craftcms.com/
 * @copyright Copyright (c) Pixel & Tonic, Inc.
 * @license https://craftcms.github.io/license/
 */

namespace craft\validators;

use Craft;
use yii\validators\Validator;

/**
 * Class UsernameValidator.
 *
 * @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
 * @since 3.0.0
 */
class UsernameValidator extends Validator
{
    /**
     * @inheritdoc
     */
    public function validateValue($value)
    {
        // Don't allow whitespace in the username
        if (preg_match('/\s+/', $value)) {
            return [Craft::t('app', '{attribute} cannot contain spaces.'), []];
        }

        return null;
    }
}

At its sim­plest form, this is all a Val­ida­tor needs to imple­ment! Giv­en some passed in $value, return whether it pass­es val­i­da­tion or not.

In this case, it’s just check­ing if it pass­es a reg­u­lar expres­sion (RegEx) test.

And indeed, we can even sim­pli­fy this fur­ther, and get rid of the cus­tom val­ida­tor alto­geth­er by using the match core val­ida­tor:

    $rules[] = [['username'], 'match', '/\s+/', 'not' => true];

Then we’re real­ly be using the plat­form, and get­ting rid of cus­tom code.

Being able to delete code is one of the most underrated joys of programming

But let’s return from our tan­gent, and see how we can lever­age these rules to our own advan­tage. Let’s say we have spe­cif­ic require­ments for our username and password fields.

Well, we can eas­i­ly extend the exist­ing mod­el val­i­da­tion rules for our User Ele­ment by lis­ten­ing for the User class trig­ger­ing the EVENT_DEFINE_RULES event: 

<?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link      https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule;

use modules\sitemodule\rules\UserRules;

use craft\elements\User;
use craft\events\DefineRulesEvent;

// ...

class SiteModule extends Module
{
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        // Add in our custom rules for the User element validation
        Event::on(
            User::class,
            User::EVENT_DEFINE_RULES,
            static function(DefineRulesEvent $event) {
                foreach(UserRules::define() as $rule) {
                    $event->rules[] = $rule;
                }
            });
    // ...
    }
}

We’re call­ing our cus­tom class method UserRules::define() to return a list of rules we want to add, and then we’re adding them one by one to the $event->rules

Here’s what the UserRules class looks like:

<?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link      https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule\rules;

use Craft;

/**
 * @author    nystudio107
 * @package   SiteModule
 * @since     1.0.0
 */
class UserRules
{
    // Constants
    // =========================================================================

    const USERNAME_MIN_LENGTH = 5;
    const USERNAME_MAX_LENGTH = 15;
    const PASSWORD_MIN_LENGTH = 7;

    // Public Methods
    // =========================================================================

    /**
     * Return an array of Yii2 validator rules to be added to the User element
     * https://www.yiiframework.com/doc/guide/2.0/en/input-validation
     *
     * @return array
     */
    public static function define(): array
    {
        return [
            [
                'username',
                'string',
                'length' => [self::USERNAME_MIN_LENGTH, self::USERNAME_MAX_LENGTH],
                'tooLong' => Craft::t(
                    'site-module',
                    'Your username {max} characters or shorter.',
                    [
                        'min' => self::USERNAME_MIN_LENGTH,
                        'max' => self::USERNAME_MAX_LENGTH
                    ]
                ),
                'tooShort' => Craft::t(
                    'site-module',
                    'Your username must {min} characters or longer.',
                    [
                        'min' => self::USERNAME_MIN_LENGTH,
                        'max' => self::USERNAME_MAX_LENGTH
                    ]
                ),
            ],
            [
                'password',
                'string',
                'min' => self::PASSWORD_MIN_LENGTH,
                'tooShort' => Craft::t(
                    'site-module',
                    'Your password must be at least {min} characters.',
                    ['min' => self::PASSWORD_MIN_LENGTH]
                )
            ],
            [
                'password',
                'match',
                'pattern' => '/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{7,})/',
                'message' => Craft::t(
                    'site-module',
                    'Your password must contain at least one of each of the following: A number, a lower-case character, an upper-case character, and a special character'
                )
            ],
        ];
    }
}

And then BOOM! Just like that we’ve extend­ed the User Ele­ment mod­el val­i­da­tion rules with our own cus­tom rules.

We’re even giv­ing it the cus­tom message to dis­play if the password field does­n’t match, as well as the mes­sage to dis­play if the username field is tooLong or tooShort.

Nice.

Cus­tom User val­i­da­tion rule error message

As you can see, we even get the dis­play of the val­i­da­tion errors for free” on the fron­tend, with­out hav­ing to do any addi­tion­al work.

The less code you write, the fewer chances you have to introduce bugs

Bear in mind that while we’re show­ing the User Ele­ment as an exam­ple, we can do this for any mod­el that Craft uses.

For instance, if you want to make the address field in Craft Com­merce required, this is your ticket!

Yii2 has Core Val­ida­tors for almost every­thing you might need; check them out before you go to write your own.

A favorite of mine is the In Core Val­ida­tor, which lets you val­i­date against spe­cif­ic val­ues. There’s even a Fil­ter Core Val­ida­tor for changing/​filtering values.

Link Models & Behaviors

But what if we want to add some prop­er­ties or meth­ods to an exist­ing mod­el? Well, we can do that, too, via Yii2 Behav­iors.

To extend our User Ele­ment with a cus­tom Behav­ior, we can lis­ten for the User class trig­ger­ing the EVENT_DEFINE_BEHAVIORS event:


<?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link      https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule;

use modules\sitemodule\behaviors\UserBehavior;

use craft\elements\User;
use craft\events\DefineBehaviorsEvent;

// ...

class SiteModule extends Module
{
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        // Add in our custom behavior for the User element
        Event::on(
            User::class,
            User::EVENT_DEFINE_BEHAVIORS,
            static function(DefineBehaviorsEvent $event) {
                $event->behaviors['userBehavior'] = ['class' => UserBehavior::class];
            });
    // ...
    }
}

Here we just add our userBehavior by set­ting the $event->behaviors['userBehavior'] to a cus­tom UserBehavior class we wrote that inher­its from the Yii2 Behav­ior class:

<?php
/**
 * Site module for Craft CMS 3.x
 *
 * Custom site module for the devMode.fm website
 *
 * @link      https://nystudio107.com
 * @copyright Copyright (c) 2020 nystudio107
 */

namespace modules\sitemodule\behaviors;

use craft\elements\User;

use yii\base\Behavior;


/**
 * @author    nystudio107
 * @package   SiteModule
 * @since     1.0.0
 */
class UserBehavior extends Behavior
{
    // Public Properties
    // =========================================================================

    // Public Methods
    // =========================================================================

    /**
     * @inheritDoc
     */
    public function events()
    {
        return [
            User::EVENT_BEFORE_SAVE => 'beforeSave',
        ];
    }

    /**
     * Save last names in upper-case
     *
     * @param $event
     */
    public function beforeSave($event)
    {
        $this->owner->lastName = mb_strtoupper($this->owner->lastName);
    }

    /**
     * Return a friendly name with a smile
     *
     * @return string
     */
    public function getHappyName()
    {
        $name = $this->owner->getFriendlyName();

        return ':) ' . $name;
    }
}

We’re using the events() method to define the Com­po­nent Events we want our behav­ior to lis­ten for.

In our case, we’re lis­ten­ing for the EVENT_BEFORE_SAVE event, and we’re call­ing a new method we added called beforeSave.

In the con­text of a behav­ior, $this->owner refers to the Mod­el object that our behav­ior is attached to; in our case, that’s a User Element.

So our beforeSave() method just upper-cas­es the User::$lastName prop­er­ty before sav­ing it. So every­one’s last name will be upper-case.

Then we’ve added a getHappyName() method that prepends a smi­ley face to the User Ele­men­t’s name, so in our Twig tem­plates we can now do:

{{ currentUser.getHappyName() }}

Pret­ty slick, we just pig­gy­backed on the exist­ing Craft User Ele­ment func­tion­al­i­ty with­out hav­ing to do a while lot of work.

In our Behav­ior, if we defined any addi­tion­al prop­er­ties, they’d be added to the User Ele­ment mod­el as well… which opens up a whole world of possibilities.

In addi­tion to writ­ing our own cus­tom behav­iors, we can also lever­age oth­er built-in Behav­iors that Yii2 offers, and add them to our own Mod­els. My per­son­al favorite is the Attrib­ut­e­Type­cast­Be­hav­ior.

Check out Zoltan’s arti­cle Extend­ing entries with Yii behav­iors in Craft 3 for even more on behaviors.

I’d also like to note what Behav­iors are not. You can not over­ride an exist­ing method with a Behav­ior. You might want over­ride an exist­ing method and replace it with your own code dynam­i­cal­ly… but Behav­iors can­not do that.

Behav­iors can only extend, not replace.

Link Wrapping Up

In addi­tion to use the plat­form”, when­ev­er we’re adding code, I think we should add as lit­tle as possible.

Yes, there are oth­er ways to add the func­tion­al­i­ty we’ve shown in this arti­cle, but the meth­ods dis­cussed here are sim­ple, and require less code.

Simple is Good

When adding code to an exist­ing project or frame­work, you typ­i­cal­ly want to go in like a sur­geon, chang­ing as lit­tle as pos­si­ble to achieve the desired effect.

Hap­py coding!