Validating Data
Before you save your data youwill probably want to ensure the data is correct and consistent. In CakePHP wehave two stages of validation:
- Before request data is converted into entities, validation rules arounddata types and formatting can be applied.
- Before data is saved, domain or application rules can be applied. These ruleshelp ensure that your application’s data remains consistent.
Validating Data Before Building Entities
When marshalling data into entities, you can validate data. Validating dataallows you to check the type, shape and size of data. By default request datawill be validated before it is converted into entities.If any validation rules fail, the returned entity will contain errors. Thefields with errors will not be present in the returned entity:
- $article = $articles->newEntity($this->request->getData());
- if ($article->errors()) {
- // Entity failed validation.
- }
New in version 3.4.0: The getErrors()
function was added.
When building an entity with validation enabled the following occurs:
- The validator object is created.
- The
table
anddefault
validation provider are attached. - The named validation method is invoked. For example
validationDefault
. - The
Model.buildValidator
event will be triggered. - Request data will be validated.
- Request data will be type-cast into types that match the column types.
- Errors will be set into the entity.
- Valid data will be set into the entity, while fields that failed validationwill be excluded.
If you’d like to disable validation when converting request data, set thevalidate
option to false:
- $article = $articles->newEntity(
- $this->request->getData(),
- ['validate' => false]
- );
The same can be said about the patchEntity()
method:
- $article = $articles->patchEntity($article, $newData, [
- 'validate' => false
- ]);
Creating A Default Validation Set
Validation rules are defined in the Table classes for convenience. This defineswhat data should be validated in conjunction with where it will be saved.
To create a default validation object in your table, create thevalidationDefault()
function:
- use Cake\ORM\Table;
- use Cake\Validation\Validator;
- class ArticlesTable extends Table
- {
- public function validationDefault(Validator $validator)
- {
- $validator
- ->requirePresence('title', 'create')
- ->notEmpty('title');
- $validator
- ->allowEmptyString('link')
- ->add('link', 'valid-url', ['rule' => 'url']);
- ...
- return $validator;
- }
- }
The available validation methods and rules come from the Validator
class andare documented in the Creating Validators section.
Note
Validation objects are intended primarily for validating user input, i.e.forms and any other posted request data.
Using A Different Validation Set
In addition to disabling validation you can choose which validation rule set youwant applied:
- $article = $articles->newEntity(
- $this->request->getData(),
- ['validate' => 'update']
- );
The above would call the validationUpdate()
method on the table instance tobuild the required rules. By default the validationDefault()
method will beused. An example validator for our articles table would be:
- class ArticlesTable extends Table
- {
- public function validationUpdate($validator)
- {
- $validator
- ->add('title', 'notEmpty', [
- 'rule' => 'notEmpty',
- 'message' => __('You need to provide a title'),
- ])
- ->add('body', 'notEmpty', [
- 'rule' => 'notEmpty',
- 'message' => __('A body is required')
- ]);
- return $validator;
- }
- }
You can have as many validation sets as necessary. See the validationchapter for more information on buildingvalidation rule-sets.
Using A Different Validation Set For Associations
Validation sets can also be defined per association. When using thenewEntity()
or patchEntity()
methods, you can pass extra options to eachof the associations to be converted:
- $data = [
- 'title' => 'My title',
- 'body' => 'The text',
- 'user_id' => 1,
- 'user' => [
- 'username' => 'mark'
- ],
- 'comments' => [
- ['body' => 'First comment'],
- ['body' => 'Second comment'],
- ]
- ];
- $article = $articles->patchEntity($article, $data, [
- 'validate' => 'update',
- 'associated' => [
- 'Users' => ['validate' => 'signup'],
- 'Comments' => ['validate' => 'custom']
- ]
- ]);
Combining Validators
Because of how validator objects are built, it is easy to break theirconstruction process into multiple reusable steps:
- // UsersTable.php
- public function validationDefault(Validator $validator)
- {
- $validator->notEmpty('username');
- $validator->notEmpty('password');
- $validator->add('email', 'valid-email', ['rule' => 'email']);
- ...
- return $validator;
- }
- public function validationHardened(Validator $validator)
- {
- $validator = $this->validationDefault($validator);
- $validator->add('password', 'length', ['rule' => ['lengthBetween', 8, 100]]);
- return $validator;
- }
Given the above setup, when using the hardened
validation set, it will alsocontain the validation rules declared in the default
set.
Validation Providers
Validation rules can use functions defined on any known providers. By defaultCakePHP sets up a few providers:
- Methods on the table class or its behaviors are available on the
table
provider. - The core
Validation\Validation
class is setup as thedefault
provider.
When a validation rule is created you can name the provider of that rule. Forexample, if your table has anisValidRole
method you can use it asa validation rule:
- use Cake\ORM\Table;
- use Cake\Validation\Validator;
- class UsersTable extends Table
- {
- public function validationDefault(Validator $validator)
- {
- $validator
- ->add('role', 'validRole', [
- 'rule' => 'isValidRole',
- 'message' => __('You need to provide a valid role'),
- 'provider' => 'table',
- ]);
- return $validator;
- }
- public function isValidRole($value, array $context)
- {
- return in_array($value, ['admin', 'editor', 'author'], true);
- }
- }
You can also use closures for validation rules:
- $validator->add('name', 'myRule', [
- 'rule' => function ($data, $provider) {
- if ($data > 1) {
- return true;
- }
- return 'Not a good value.';
- }
- ]);
Validation methods can return error messages when they fail. This is a simpleway to make error messages dynamic based on the provided value.
Getting Validators From Tables
Once you have created a few validation sets in your table class, you can get theresulting object by name:
- $defaultValidator = $usersTable->validator('default');
- $hardenedValidator = $usersTable->validator('hardened');
Deprecated since version 3.5.0: validator()
is deprecated. Use getValidator()
instead.
Default Validator Class
As stated above, by default the validation methods receive an instance ofCake\Validation\Validator
. Instead, if you want your custom validator’sinstance to be used each time, you can use table’s $_validatorClass
property:
- // In your table class
- public function initialize(array $config)
- {
- $this->_validatorClass = '\FullyNamespaced\Custom\Validator';
- }
Applying Application Rules
While basic data validation is done when request data is converted intoentities, many applications also have more complexvalidation that should only be applied after basic validation has completed.
Where validation ensures the form or syntax of your data is correct, rulesfocus on comparing data against the existing state of your application and/ornetwork.
These types of rules are often referred to as ‘domain rules’ or ‘applicationrules’. CakePHP exposes this concept through ‘RulesCheckers’ which are appliedbefore entities are persisted. Some example domain rules are:
- Ensuring email uniqueness
- State transitions or workflow steps (e.g., updating an invoice’s status).
- Preventing the modification of soft deleted items.
- Enforcing usage/rate limit caps.
Domain rules are checked when calling the Tablesave()
anddelete()
methods.
Creating a Rules Checker
Rules checker classes are generally defined by the buildRules()
method in yourtable class. Behaviors and other event subscribers can use theModel.buildRules
event to augment the rules checker for a given Tableclass:
- use Cake\ORM\RulesChecker;
- // In a table class
- public function buildRules(RulesChecker $rules)
- {
- // Add a rule that is applied for create and update operations
- $rules->add(function ($entity, $options) {
- // Return a boolean to indicate pass/failure
- }, 'ruleName');
- // Add a rule for create.
- $rules->addCreate(function ($entity, $options) {
- // Return a boolean to indicate pass/failure
- }, 'ruleName');
- // Add a rule for update
- $rules->addUpdate(function ($entity, $options) {
- // Return a boolean to indicate pass/failure
- }, 'ruleName');
- // Add a rule for the deleting.
- $rules->addDelete(function ($entity, $options) {
- // Return a boolean to indicate pass/failure
- }, 'ruleName');
- return $rules;
- }
Your rules functions can expect to get the Entity being checked and an array ofoptions. The options array will contain errorField
, message
, andrepository
. The repository
option will contain the table class the rulesare attached to. Because rules accept any callable
, you can also useinstance functions:
- $rules->addCreate([$this, 'uniqueEmail'], 'uniqueEmail');
or callable classes:
- $rules->addCreate(new IsUnique(['email']), 'uniqueEmail');
When adding rules you can define the field the rule is for and the errormessage as options:
- $rules->add([$this, 'isValidState'], 'validState', [
- 'errorField' => 'status',
- 'message' => 'This invoice cannot be moved to that status.'
- ]);
The error will be visible when calling the errors()
method on the entity:
- $entity->errors(); // Contains the domain rules error messages
Creating Unique Field Rules
Because unique rules are quite common, CakePHP includes a simple Rule class thatallows you to define unique field sets:
- use Cake\ORM\Rule\IsUnique;
- // A single field.
- $rules->add($rules->isUnique(['email']));
- // A list of fields
- $rules->add($rules->isUnique(
- ['username', 'account_id'],
- 'This username & account_id combination has already been used.'
- ));
When setting rules on foreign key fields it is important to remember, thatonly the fields listed are used in the rule. This means that setting$user->account->id
will not trigger the above rule.
Foreign Key Rules
While you could rely on database errors to enforce constraints, using rules codecan help provide a nicer user experience. Because of this CakePHP includes anExistsIn
rule class:
- // A single field.
- $rules->add($rules->existsIn('article_id', 'Articles'));
- // Multiple keys, useful for composite primary keys.
- $rules->add($rules->existsIn(['site_id', 'article_id'], 'Articles'));
The fields to check existence against in the related table must be part of theprimary key.
You can enforce existsIn
to pass when nullable parts of your composite foreign keyare null:
- // Example: A composite primary key within NodesTable is (id, site_id).
- // A Node may reference a parent Node but does not need to. In latter case, parent_id is null.
- // Allow this rule to pass, even if fields that are nullable, like parent_id, are null:
- $rules->add($rules->existsIn(
- ['parent_id', 'site_id'], // Schema: parent_id NULL, site_id NOT NULL
- 'ParentNodes',
- ['allowNullableNulls' => true]
- ));
- // A Node however should in addition also always reference a Site.
- $rules->add($rules->existsIn(['site_id'], 'Sites'));
In most SQL databases multi-column UNIQUE
indexes allow multiple null valuesto exist as NULL
is not equal to itself. While, allowing multiple nullvalues is the default behavior of CakePHP, you can include null values in yourunique checks using allowMultipleNulls
:
- // Only one null value can exist in `parent_id` and `site_id`
- $rules->add($rules->existsIn(
- ['parent_id', 'site_id'],
- 'ParentNodes',
- ['allowMultipleNulls' => false]
- ));
New in version 3.3.0: The allowNullableNulls
and allowMultipleNulls
options were added.
Association Count Rules
If you need to validate that a property or association contains the correctnumber of values, you can use the validCount()
rule:
- // In the ArticlesTable.php file
- // No more than 5 tags on an article.
- $rules->add($rules->validCount('tags', 5, '<=', 'You can only have 5 tags'));
When defining count based rules, the third parameter lets you define thecomparison operator to use. ==
, >=
, <=
, >
, <
, and !=
are the accepted operators. To ensure a property’s count is within a range, usetwo rules:
- // In the ArticlesTable.php file
- // Between 3 and 5 tags
- $rules->add($rules->validCount('tags', 3, '>=', 'You must have at least 3 tags'));
- $rules->add($rules->validCount('tags', 5, '<=', 'You must have at most 5 tags'));
Note that validCount
returns false
if the property is not countable or does not exist:
- // The save operation will fail if tags is null.
- $rules->add($rules->validCount('tags', 0, '<=', 'You must not have any tags'));
New in version 3.3.0: The validCount()
method was added in 3.3.0.
Using Entity Methods as Rules
You may want to use entity methods as domain rules:
- $rules->add(function ($entity, $options) {
- return $entity->isOkLooking();
- }, 'ruleName');
Using Conditional Rules
You may want to conditionally apply rules based on entity data:
- $rules->add(function ($entity, $options) use($rules) {
- if ($entity->role == 'admin') {
- $rule = $rules->existsIn('user_id', 'Admins');
- return $rule($entity, $options);
- }
- if ($entity->role == 'user') {
- $rule = $rules->existsIn('user_id', 'Users');
- return $rule($entity, $options);
- }
- return false;
- }, 'userExists');
Conditional/Dynamic Error Messages
Rules, being it custom callables, orrule objects, can either return a boolean, indicatingwhether they passed, or they can return a string, which means that the rule did not pass,and that the returned string should be used as the error message.
Possible existing error messages defined via the message
option will be overwrittenby the ones returned from the rule:
- $rules->add(
- function ($entity, $options) {
- if (!$entity->length) {
- return false;
- }
- if ($entity->length < 10) {
- return 'Error message when value is less than 10';
- }
- if ($entity->length > 20) {
- return 'Error message when value is greater than 20';
- }
- return true;
- },
- 'ruleName',
- [
- 'errorField' => 'length',
- 'message' => 'Generic error message used when `false` is returned'
- ]
- );
Note
Note that in order for the returned message to be actually used, you must also supply theerrorField
option, otherwise the rule will just silently fail to pass, ie without anerror message being set on the entity!
Creating Custom re-usable Rules
You may want to re-use custom domain rules. You can do so by creating your own invokable rule:
- use App\ORM\Rule\IsUniqueWithNulls;
- // ...
- public function buildRules(RulesChecker $rules)
- {
- $rules->add(new IsUniqueWithNulls(['parent_id', 'instance_id', 'name']), 'uniqueNamePerParent', [
- 'errorField' => 'name',
- 'message' => 'Name must be unique per parent.'
- ]);
- return $rules;
- }
See the core rules for examples on how to create such rules.
Creating Custom Rule Objects
If your application has rules that are commonly reused, it is helpful to packagethose rules into re-usable classes:
- // in src/Model/Rule/CustomRule.php
- namespace App\Model\Rule;
- use Cake\Datasource\EntityInterface;
- class CustomRule
- {
- public function __invoke(EntityInterface $entity, array $options)
- {
- // Do work
- return false;
- }
- }
- // Add the custom rule
- use App\Model\Rule\CustomRule;
- $rules->add(new CustomRule(...), 'ruleName');
By creating custom rule classes you can keep your code DRY and make your domainrules easy to test.
Disabling Rules
When saving an entity, you can disable the rules if necessary:
- $articles->save($article, ['checkRules' => false]);
Validation vs. Application Rules
The CakePHP ORM is unique in that it uses a two-layered approach to validation.
The first layer is validation. Validation rules are intended to operate ina stateless way. They are best leveraged to ensure that the shape, data typesand format of data is correct.
The second layer is application rules. Application rules are best leveraged tocheck stateful properties of your entities. For example, validation rules couldensure that an email address is valid, while an application rule could ensurethat the email address is unique.
As you already discovered, the first layer is done through the Validator
objects when calling newEntity()
or patchEntity()
:
- $validatedEntity = $articlesTable->newEntity(
- $unsafeData,
- ['validate' => 'customName']
- );
- $validatedEntity = $articlesTable->patchEntity(
- $entity,
- $unsafeData,
- ['validate' => 'customName']
- );
In the above example, we’ll use a ‘custom’ validator, which is defined using thevalidationCustomName()
method:
- public function validationCustomName($validator)
- {
- $validator->add(...);
- return $validator;
- }
Validation assumes strings or array are passed since that is what is receivedfrom any request:
- // In src/Model/Table/UsersTable.php
- public function validatePasswords($validator)
- {
- $validator->add('confirm_password', 'no-misspelling', [
- 'rule' => ['compareWith', 'password'],
- 'message' => 'Passwords are not equal',
- ]);
- ...
- return $validator;
- }
Validation is not triggered when directly setting properties on yourentities:
- $userEntity->email = 'not an email!!';
- $usersTable->save($userEntity);
In the above example the entity will be saved as validation is onlytriggered for the newEntity()
and patchEntity()
methods. The secondlevel of validation is meant to address this situation.
Application rules as explained above will be checked whenever save()
ordelete()
are called:
- // In src/Model/Table/UsersTable.php
- public function buildRules(RulesChecker $rules)
- {
- $rules->add($rules->isUnique('email'));
- return $rules;
- }
- // Elsewhere in your application code
- $userEntity->email = 'a@duplicated.email';
- $usersTable->save($userEntity); // Returns false
While Validation is meant for direct user input, application rules are specificfor data transitions generated inside your application:
- // In src/Model/Table/OrdersTable.php
- public function buildRules(RulesChecker $rules)
- {
- $check = function($order) {
- if($order->shipping_mode !== 'free'){
- return true;
- }
- return $order->price >= 100;
- };
- $rules->add($check, [
- 'errorField' => 'shipping_mode',
- 'message' => 'No free shipping for orders under 100!'
- ]);
- return $rules;
- }
- // Elsewhere in application code
- $order->price = 50;
- $order->shipping_mode = 'free';
- $ordersTable->save($order); // Returns false
Using Validation as Application Rules
In certain situations you may want to run the same data validation routines fordata that was both generated by users and inside your application. This couldcome up when running a CLI script that directly sets properties on entities:
- // In src/Model/Table/UsersTable.php
- public function validationDefault(Validator $validator)
- {
- $validator->add('email', 'valid', [
- 'rule' => 'email',
- 'message' => 'Invalid email'
- ]);
- ...
- return $validator;
- }
- public function buildRules(RulesChecker $rules)
- {
- // Add validation rules
- $rules->add(function($entity) {
- $data = $entity->extract($this->schema()->columns(), true);
- $validator = $this->validator('default');
- $errors = $validator->errors($data, $entity->isNew());
- $entity->errors($errors);
- return empty($errors);
- });
- ...
- return $rules;
- }
When executed the save will fail thanks to the new application rule thatwas added:
- $userEntity->email = 'not an email!!!';
- $usersTable->save($userEntity);
- $userEntity->errors('email'); // Invalid email
The same result can be expected when using newEntity()
orpatchEntity()
:
- $userEntity = $usersTable->newEntity(['email' => 'not an email!!']);
- $userEntity->errors('email'); // Invalid email