DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

Extending Craft CMS with Validation Rules and Behaviors

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!

Andrew Welch / nystudio107

Craft cms rules behaviors yii2 module

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.

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.

Mod­els & 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.

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.

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.

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.

Craft cms model validation rules frontend

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.

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!

Mod­els & 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.

Wrap­ping 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.

Code with precision like a surgeon

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.

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!

Further Reading

If you want to be notified about new articles, follow nystudio107 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Instrument, monitor, fix: a hands-on debugging session

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️