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

Top comments (0)