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:

  1. $article = $articles->newEntity($this->request->getData());
  2. if ($article->errors()) {
  3. // Entity failed validation.
  4. }

When building an entity with validation enabled the following occurs:

  • The validator object is created.
  • The table and default 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:
  1. $article = $articles->newEntity(
  2. $this->request->getData(),
  3. ['validate' => false]
  4. );

The same can be said about the patchEntity() method:

  1. $article = $articles->patchEntity($article, $newData, [
  2. 'validate' => false
  3. ]);

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:

  1. use Cake\ORM\Table;
  2. use Cake\Validation\Validator;
  3.  
  4. class ArticlesTable extends Table
  5. {
  6. public function validationDefault(Validator $validator)
  7. {
  8. $validator
  9. ->requirePresence('title', 'create')
  10. ->notEmptyString('title');
  11.  
  12. $validator
  13. ->allowEmptyString('link')
  14. ->add('link', 'valid-url', ['rule' => 'url']);
  15.  
  16. ...
  17.  
  18. return $validator;
  19. }
  20. }

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:

  1. $article = $articles->newEntity(
  2. $this->request->getData(),
  3. ['validate' => 'update']
  4. );

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:

  1. class ArticlesTable extends Table
  2. {
  3. public function validationUpdate($validator)
  4. {
  5. $validator
  6. ->notEmptyString('title', __('You need to provide a title'))
  7. ->notEmptyString('body', __('A body is required'));
  8. return $validator;
  9. }
  10. }

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:

  1. $data = [
  2. 'title' => 'My title',
  3. 'body' => 'The text',
  4. 'user_id' => 1,
  5. 'user' => [
  6. 'username' => 'mark'
  7. ],
  8. 'comments' => [
  9. ['body' => 'First comment'],
  10. ['body' => 'Second comment'],
  11. ]
  12. ];
  13.  
  14. $article = $articles->patchEntity($article, $data, [
  15. 'validate' => 'update',
  16. 'associated' => [
  17. 'Users' => ['validate' => 'signup'],
  18. 'Comments' => ['validate' => 'custom']
  19. ]
  20. ]);

Combining Validators

Because of how validator objects are built, it is easy to break theirconstruction process into multiple reusable steps:

  1. // UsersTable.php
  2.  
  3. public function validationDefault(Validator $validator)
  4. {
  5. $validator->notEmptyString('username');
  6. $validator->notEmptyString('password');
  7. $validator->add('email', 'valid-email', ['rule' => 'email']);
  8. ...
  9.  
  10. return $validator;
  11. }
  12.  
  13. public function validationHardened(Validator $validator)
  14. {
  15. $validator = $this->validationDefault($validator);
  16.  
  17. $validator->add('password', 'length', ['rule' => ['lengthBetween', 8, 100]]);
  18. return $validator;
  19. }

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 tableprovider.
  • 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 an isValidRole method you can use it asa validation rule:
  1. use Cake\ORM\Table;
  2. use Cake\Validation\Validator;
  3.  
  4. class UsersTable extends Table
  5. {
  6. public function validationDefault(Validator $validator)
  7. {
  8. $validator
  9. ->add('role', 'validRole', [
  10. 'rule' => 'isValidRole',
  11. 'message' => __('You need to provide a valid role'),
  12. 'provider' => 'table',
  13. ]);
  14. return $validator;
  15. }
  16.  
  17. public function isValidRole($value, array $context)
  18. {
  19. return in_array($value, ['admin', 'editor', 'author'], true);
  20. }
  21.  
  22. }

You can also use closures for validation rules:

  1. $validator->add('name', 'myRule', [
  2. 'rule' => function ($data, $provider) {
  3. if ($data > 1) {
  4. return true;
  5. }
  6. return 'Not a good value.';
  7. }
  8. ]);

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:

  1. $defaultValidator = $usersTable->getValidator('default');
  2.  
  3. $hardenedValidator = $usersTable->getValidator('hardened');

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:

  1. // In your table class
  2. public function initialize(array $config): void
  3. {
  4. $this->_validatorClass = '\FullyNamespaced\Custom\Validator';
  5. }

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 Table save() and delete() 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:

  1. use Cake\ORM\RulesChecker;
  2.  
  3. // In a table class
  4. public function buildRules(RulesChecker $rules)
  5. {
  6. // Add a rule that is applied for create and update operations
  7. $rules->add(function ($entity, $options) {
  8. // Return a boolean to indicate pass/failure
  9. }, 'ruleName');
  10.  
  11. // Add a rule for create.
  12. $rules->addCreate(function ($entity, $options) {
  13. // Return a boolean to indicate pass/failure
  14. }, 'ruleName');
  15.  
  16. // Add a rule for update
  17. $rules->addUpdate(function ($entity, $options) {
  18. // Return a boolean to indicate pass/failure
  19. }, 'ruleName');
  20.  
  21. // Add a rule for the deleting.
  22. $rules->addDelete(function ($entity, $options) {
  23. // Return a boolean to indicate pass/failure
  24. }, 'ruleName');
  25.  
  26. return $rules;
  27. }

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:

  1. $rules->addCreate([$this, 'uniqueEmail'], 'uniqueEmail');

or callable classes:

  1. $rules->addCreate(new IsUnique(['email']), 'uniqueEmail');

When adding rules you can define the field the rule is for and the errormessage as options:

  1. $rules->add([$this, 'isValidState'], 'validState', [
  2. 'errorField' => 'status',
  3. 'message' => 'This invoice cannot be moved to that status.'
  4. ]);

The error will be visible when calling the errors() method on the entity:

  1. $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:

  1. use Cake\ORM\Rule\IsUnique;
  2.  
  3. // A single field.
  4. $rules->add($rules->isUnique(['email']));
  5.  
  6. // A list of fields
  7. $rules->add($rules->isUnique(
  8. ['username', 'account_id'],
  9. 'This username & account_id combination has already been used.'
  10. ));

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:

  1. // A single field.
  2. $rules->add($rules->existsIn('article_id', 'Articles'));
  3.  
  4. // Multiple keys, useful for composite primary keys.
  5. $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:

  1. // Example: A composite primary key within NodesTable is (id, site_id).
  2. // A Node may reference a parent Node but does not need to. In latter case, parent_id is null.
  3. // Allow this rule to pass, even if fields that are nullable, like parent_id, are null:
  4. $rules->add($rules->existsIn(
  5. ['parent_id', 'site_id'], // Schema: parent_id NULL, site_id NOT NULL
  6. 'ParentNodes',
  7. ['allowNullableNulls' => true]
  8. ));
  9.  
  10. // A Node however should in addition also always reference a Site.
  11. $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:

  1. // Only one null value can exist in `parent_id` and `site_id`
  2. $rules->add($rules->existsIn(
  3. ['parent_id', 'site_id'],
  4. 'ParentNodes',
  5. ['allowMultipleNulls' => false]
  6. ));

Association Count Rules

If you need to validate that a property or association contains the correctnumber of values, you can use the validCount() rule:

  1. // In the ArticlesTable.php file
  2. // No more than 5 tags on an article.
  3. $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:

  1. // In the ArticlesTable.php file
  2. // Between 3 and 5 tags
  3. $rules->add($rules->validCount('tags', 3, '>=', 'You must have at least 3 tags'));
  4. $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:

  1. // The save operation will fail if tags is null.
  2. $rules->add($rules->validCount('tags', 0, '<=', 'You must not have any tags'));

The LinkConstraint lets you emulate SQL constraints in databases that don’tsupport them, or when you want to provide more user friendly error messages whenconstraints would fail. This rule enables you to check if an association does or does nothave related records depending on the mode used:

  1. // Ensure that each comment is linked to an Article during updates.
  2. $rules->addUpdate($rules->isLinkedTo(
  3. 'Articles',
  4. 'article',
  5. 'Requires an article'
  6. ));
  7.  
  8. // Ensure that an article has no linked comments during delete.
  9. $rules->addDelete($rules->isNotLinkedTo(
  10. 'Comments',
  11. 'comments',
  12. 'Must have zero comments before deletion.'
  13. ));

New in version 4.0.0.

Using Entity Methods as Rules

You may want to use entity methods as domain rules:

  1. $rules->add(function ($entity, $options) {
  2. return $entity->isOkLooking();
  3. }, 'ruleName');

Using Conditional Rules

You may want to conditionally apply rules based on entity data:

  1. $rules->add(function ($entity, $options) use($rules) {
  2. if ($entity->role == 'admin') {
  3. $rule = $rules->existsIn('user_id', 'Admins');
  4.  
  5. return $rule($entity, $options);
  6. }
  7. if ($entity->role == 'user') {
  8. $rule = $rules->existsIn('user_id', 'Users');
  9.  
  10. return $rule($entity, $options);
  11. }
  12.  
  13. return false;
  14. }, '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:

  1. $rules->add(
  2. function ($entity, $options) {
  3. if (!$entity->length) {
  4. return false;
  5. }
  6.  
  7. if ($entity->length < 10) {
  8. return 'Error message when value is less than 10';
  9. }
  10.  
  11. if ($entity->length > 20) {
  12. return 'Error message when value is greater than 20';
  13. }
  14.  
  15. return true;
  16. },
  17. 'ruleName',
  18. [
  19. 'errorField' => 'length',
  20. 'message' => 'Generic error message used when `false` is returned'
  21. ]
  22. );

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:

  1. use App\ORM\Rule\IsUniqueWithNulls;
  2. // ...
  3. public function buildRules(RulesChecker $rules)
  4. {
  5. $rules->add(new IsUniqueWithNulls(['parent_id', 'instance_id', 'name']), 'uniqueNamePerParent', [
  6. 'errorField' => 'name',
  7. 'message' => 'Name must be unique per parent.'
  8. ]);
  9. return $rules;
  10. }

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:

  1. // in src/Model/Rule/CustomRule.php
  2. namespace App\Model\Rule;
  3.  
  4. use Cake\Datasource\EntityInterface;
  5.  
  6. class CustomRule
  7. {
  8. public function __invoke(EntityInterface $entity, array $options)
  9. {
  10. // Do work
  11. return false;
  12. }
  13. }
  14.  
  15. // Add the custom rule
  16. use App\Model\Rule\CustomRule;
  17.  
  18. $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:

  1. $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 Validatorobjects when calling newEntity() or patchEntity():

  1. $validatedEntity = $articlesTable->newEntity(
  2. $unsafeData,
  3. ['validate' => 'customName']
  4. );
  5. $validatedEntity = $articlesTable->patchEntity(
  6. $entity,
  7. $unsafeData,
  8. ['validate' => 'customName']
  9. );

In the above example, we’ll use a ‘custom’ validator, which is defined using thevalidationCustomName() method:

  1. public function validationCustomName($validator)
  2. {
  3. $validator->add(
  4. // ...
  5. );
  6.  
  7. return $validator;
  8. }

Validation assumes strings or array are passed since that is what is receivedfrom any request:

  1. // In src/Model/Table/UsersTable.php
  2. public function validatePasswords($validator)
  3. {
  4. $validator->add('confirm_password', 'no-misspelling', [
  5. 'rule' => ['compareWith', 'password'],
  6. 'message' => 'Passwords are not equal',
  7. ]);
  8.  
  9. // ...
  10.  
  11. return $validator;
  12. }

Validation is not triggered when directly setting properties on yourentities:

  1. $userEntity->email = 'not an email!!';
  2. $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:

  1. // In src/Model/Table/UsersTable.php
  2. public function buildRules(RulesChecker $rules)
  3. {
  4. $rules->add($rules->isUnique('email'));
  5.  
  6. return $rules;
  7. }
  8.  
  9. // Elsewhere in your application code
  10. $userEntity->email = 'a@duplicated.email';
  11. $usersTable->save($userEntity); // Returns false

While Validation is meant for direct user input, application rules are specificfor data transitions generated inside your application:

  1. // In src/Model/Table/OrdersTable.php
  2. public function buildRules(RulesChecker $rules)
  3. {
  4. $check = function($order) {
  5. if($order->shipping_mode !== 'free'){
  6. return true;
  7. }
  8.  
  9. return $order->price >= 100;
  10. };
  11. $rules->add($check, [
  12. 'errorField' => 'shipping_mode',
  13. 'message' => 'No free shipping for orders under 100!'
  14. ]);
  15.  
  16. return $rules;
  17. }
  18.  
  19. // Elsewhere in application code
  20. $order->price = 50;
  21. $order->shipping_mode = 'free';
  22. $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:

  1. // In src/Model/Table/UsersTable.php
  2. public function validationDefault(Validator $validator)
  3. {
  4. $validator->add('email', 'valid_email', [
  5. 'rule' => 'email',
  6. 'message' => 'Invalid email'
  7. ]);
  8.  
  9. // ...
  10.  
  11. return $validator;
  12. }
  13.  
  14. public function buildRules(RulesChecker $rules)
  15. {
  16. // Add validation rules
  17. $rules->add(function($entity) {
  18. $data = $entity->extract($this->schema()->columns(), true);
  19. $validator = $this->validator('default');
  20. $errors = $validator->validate($data, $entity->isNew());
  21. $entity->errors($errors);
  22.  
  23. return empty($errors);
  24. });
  25.  
  26. // ...
  27.  
  28. return $rules;
  29. }

When executed the save will fail thanks to the new application rule thatwas added:

  1. $userEntity->email = 'not an email!!!';
  2. $usersTable->save($userEntity);
  3. $userEntity->errors('email'); // Invalid email

The same result can be expected when using newEntity() orpatchEntity():

  1. $userEntity = $usersTable->newEntity(['email' => 'not an email!!']);
  2. $userEntity->errors('email'); // Invalid email