Extending Craft CMS with Validation Rules and Behaviors
Craft CMS is a web application that is amazingly flexible & customizable using the built-in functionality that the platform offers. Use the platform!
Andrew Welch / nystudio107
Craft CMS is built on the rock-solid Yii2 framework, which is something you normally don’t need to think about. It just works, as it should.
But there are times that you need or want to extend the platform into something truly custom, which we looked at in the Enhancing a Craft CMS 3 Website with a Custom Module article.
In this article, we’ll talk about two ways you can use the platform that you normally don’t even have to think about to your advantage.
Something we commonly hear in frontend development is to “use the platform”, which I think is fantastic advice. Why re-invent an elaborate custom setup when the platform already provides you with a battle-worn way to accomplish your goals?
The same holds true with any kind of development. If you’ve writing something on top of a platform — whatever that platform may be — I think it always makes sense to try to leverage it as much as possible.
It’s there. It’s well thought-out. It’s tested. Use it!
When we’re using Craft CMS, we’re also using the Yii2 platform that it’s built on. Indeed, as we discussed in the So You Wanna Make a Craft 3 Plugin? article, to know Craft plugin development, you will want to learn some part of Yii2.
So let’s do just that! The Yii2 documentation is a great place to start.
Models & Rules
Models are a core building block of Yii2, and so also Craft CMS. They are at the core of the Model-View-Controller (MVC) paradigm that many frameworks use.
Models are used to represent data and validate data via a set of rules. For instance, Craft CMS has a User element (which is also a model) that encapsulates all of the data needed to represent a User in Craft CMS.
It also has validation 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 config than code to you, then you’d be right! Model validation rules are essentially a list of rules that the data must pass in order to be considered valid.
Yii2 has a base Validator class to help you write validators, and ships with a whole bunch of useful Core Validators 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 validation rule is an array:
- Field — the model field (aka attribute or object property) or array of model fields to apply this validation rule to
- Validator — the validator to use, which can be a Validator class, an alias to a validator class, PHP Callable, or even an anonymous function for inline validation
- [params] — depending on the validator, there may be additional optional parameters you can define
So in the above User Element example, the email & unverifiedEmail fields are using the built-in email core validator that Yii2 provides.
The username has several validation rules listed, which are applied in order:
- string — This validator checks if the input value is a valid string with certain length (100 in this case)
- required — This validator checks if the input value is provided and not empty
- UsernameValidator — This is a custom validator that P&T wrote to handle validating the username field
The username field actually gives us a fun little tangent we can go on, so let’s peek under the hood to see how simple it can be to write a custom 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 simplest form, this is all a Validator needs to implement! Given some passed in $value, return whether it passes validation or not.
In this case, it’s just checking if it passes a regular expression (RegEx) test.
And indeed, we can even simplify this further, and get rid of the custom validator altogether by using the match core validator:
$rules[] = [['username'], 'match', '/\s+/', 'not' => true];
Then we’re really be using the platform, and getting rid of custom code.
But let’s return from our tangent, and see how we can leverage these rules to our own advantage. Let’s say we have specific requirements for our username and password fields.
Well, we can easily extend the existing model validation rules for our User Element by listening for the User class triggering 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 calling our custom 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 extended the User Element model validation rules with our own custom rules.
We’re even giving it the custom message to display if the password field doesn’t match, as well as the message to display if the username field is tooLong or tooShort.
Nice.
As you can see, we even get the display of the validation errors “for free” on the frontend, without having to do any additional work.
Bear in mind that while we’re showing the User Element as an example, we can do this for any model that Craft uses.
For instance, if you want to make the address field in Craft Commerce required, this is your ticket!
Models & Behaviors
But what if we want to add some properties or methods to an existing model? Well, we can do that, too, via Yii2 Behaviors.
To extend our User Element with a custom Behavior, we can listen for the User class triggering 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 setting the $event->behaviors['userBehavior'] to a custom UserBehavior class we wrote that inherits from the Yii2 Behavior 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 Component Events we want our behavior to listen for.
In our case, we’re listening for the EVENT_BEFORE_SAVE event, and we’re calling a new method we added called beforeSave.
In the context of a behavior, $this->owner refers to the Model object that our behavior is attached to; in our case, that’s a User Element.
So our beforeSave() method just upper-cases the User::$lastName property before saving it. So everyone’s last name will be upper-case.
Then we’ve added a getHappyName() method that prepends a smiley face to the User Element’s name, so in our Twig templates we can now do:
{{ currentUser.getHappyName() }}
Pretty slick, we just piggybacked on the existing Craft User Element functionality without having to do a while lot of work.
In our Behavior, if we defined any additional properties, they’d be added to the User Element model as well… which opens up a whole world of possibilities.
In addition to writing our own custom behaviors, we can also leverage other built-in Behaviors that Yii2 offers, and add them to our own Models. My personal favorite is the AttributeTypecastBehavior.
Check out Zoltan’s article Extending entries with Yii behaviors in Craft 3 for even more on behaviors.
I’d also like to note what Behaviors are not. You can not override an existing method with a Behavior. You might want override an existing method and replace it with your own code dynamically… but Behaviors cannot do that.
Behaviors can only extend, not replace.
Wrapping Up
In addition to “use the platform”, whenever we’re adding code, I think we should add as little as possible.
Yes, there are other ways to add the functionality we’ve shown in this article, but the methods discussed here are simple, and require less code.
When adding code to an existing project or framework, you typically want to go in like a surgeon, changing as little as possible to achieve the desired effect.
Happy 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)