Validation
Table of contents
Loading...Introduction
Every form or value that users submit should be validated on the server to make sure that its in the expected format and prevent malicious data from being inserted into the database.
CakePHP's lightweight validation library makes validation quite easy and straightforward. It has a flexible ruling system that supports a wide range of validation rules out of the box.
composer require cakephp/validation
Performing validation
Validator instantiation
To begin the validation process, a validator object has to be instantiated.
use Cake\Validation\Validator;
// ...
$validator = new Validator();
Adding validation rules
Validation rules can be added to the validator object with chained method calls that stand for the wanted rules. The first argument is always the field name, and the others vary on the validation rule of the method.
Require presence
There is an important distinction to be made between if a field key is required to be present in the submitted values or if the field value is required to not be empty.
The requirePresence()
method is used to check if a field key is present but not if its empty.
An empty string, for instance, is considered as present.
Null values are another story.
Null values
If the value is null
and the only rule is requirePresence()
, it creates an error
"This field cannot be left empty".
This is because the Cake validation library automatically sets a rule that field values cannot
be null as soon as there is any validation rule set for the field that doesn't start
with allowEmpty...()
in which case null values are allowed.
requirePresence()
, maxLength()
, numeric()
, email()
, date()
etc. have the same underlying non-null rule.
It's important to be aware of this when dealing with optional values, and sadly, this isn't well documented.
Create and update mode
Required values during a creation request may not be required to be present in the validated array
during an update when only the updated key-value is sent to the server.
For this distinction, a bool isCreateMode
can be added to the requirePresence
rule as
a second argument:
requirePresence('field_name', $isCreateMode, 'Custom error message')
If $isCreateMode
is false
, the field presence is not required.
Note: when submitting a form, radio buttons, checkboxes and potentially other fields are not sent over by the browser if they don't contain a value, so they should never be required to be present if they're optional.
Optional fields
I would recommend making the backend flexible and accepting both null
and empty strings
for optional values.
Especially because there is no out-of-the-box rule for specifically allowing null values.
We have to use the allowEmptyString
method which allows for an empty string, null
and potentially other values such as false
, 0
and "0"
but I haven't found
documentation
on this.
Required fields
Besides requirePresence()
that should be used, I recommend using a notEmpty...()
rule to
clearly define that the field value must not be empty.
Also, it permits setting a custom error message that can be translated.
Custom rules
Custom rules can be added with the add()
method. The first parameter is the field name,
the second is the name of the custom rule, and the third is an array with the rule options.
The rule
key is required and can contain a closure that receives two arguments. The first one
is the value that is being validated with this rule and the second one a context array
containing, among other things, the array of all values being validated accessible via $context['data']
.
$extra = 'Some optional additional value for the closure';
$validator->add('title', 'custom', [
'rule' => function ($value, $context) use ($extra) {
// Custom logic that returns true if the validation passes and
// false if the error message below should be shown
},
'message' => 'The title is not valid'
]);
For more complex custom rules, please check out the documentation for custom rules.
Executing rule only if the previous one has succeeded
From Marking Rules as the Last to Run:
When fields have multiple rules, each validation rule will be run even if the previous one has failed. This allows you to collect as many validation errors as you can in a single pass. If you want to stop execution after a specific rule has failed, you can set the
last
option to true:
$validator
->add('body', [
'minLength' => [
'rule' => ['minLength', 10],
// This rule must succeed before proceeding to the next one
'last' => true,
'message' => 'Minimum length 10.',
],
'maxLength' => [
'rule' => ['maxLength', 250],
'message' => 'Maximum length 250.',
],
]);
Example
This is an example of a simplified validator object that validates the request body of a client creation request.
The error messages are translated with the __()
function but this is independent of the validation process.
More on translations here.
$validator
// Require presence indicates that the Field is required in the request body
// When second parameter "mode" is false, the field presence is not required
->requirePresence('first_name', $isCreateMode, __('Field presence is required'))
// Required field first_name
->notEmptyString('first_name', __('Required'))
->minLength('first_name', 2, __('Minimum length is 2'))
->maxLength('first_name', 100, __('Maximum length is 100'))
// Optional field last_name
->requirePresence('last_name', $isCreateMode, __('Field is required'))
// allowEmptyString to enable null and empty string because cakephp validation library automatically
// sets a rule that the field cannot be null when there is another validation rule set for the field.
->allowEmptyString('last_name')
->minLength('last_name', 2, __('Minimum length is 2'))
->maxLength('last_name', 100, __('Maximum length is 100'))
// E-mail
->requirePresence('email', $isCreateMode, __('Field is required'))
->allowEmptyString('email')
->email('email', false, __('Invalid email'))
// Birthdate date field with different formats and custom validation rule checking that it's not in the future
->requirePresence('birthdate', $isCreateMode, __('Field is required'))
->allowEmptyDate('birthdate') // Not required, allow null and empty string
->add('birthdate', [
'date' => [
'rule' => ['date', ['ymd', 'mdy', 'dmy']],
'message' => __('Invalid date value'),
// This rule must succeed before proceeding to the next one
'last' => true,
],
'validateNotInFuture' => [
'rule' => function ($value, $context) {
$today = new \DateTime();
$birthdate = new \DateTime($value);
// Check that birthdate is not in the future
return $birthdate <= $today;
},
'message' => __('Cannot be in the future'),
],
])
// Sex checkbox options
// Optional checkbox key may not be sent over by the browser if not set, so second param must be false
->requirePresence('sex', false)
->allowEmptyString('sex')
->inList('sex', ['M', 'F', 'O', ''], 'Invalid option')
// Client status select options
->requirePresence('client_status_id', $isCreateMode, __('Field is required'))
->notEmptyString('client_status_id', __('Required'))
->numeric('client_status_id', __('Invalid option format'))
->add('client_status_id', 'exists', [
'rule' => function ($value, $context) {
return $this->clientStatusFinderRepository->clientStatusExists((int)$value);
},
'message' => __('Invalid option'),
])
;
Executing the validation
To perform the validation and get the result, the validate()
method must be called on the validator object
with as argument the data to be validated.
The expected data format is an associative array with the field names as keys and the values as values.
It returns an array with the validation errors if it fails or an empty array otherwise.
// Perform validation with the client creation values retrieved from the request body
$errors = $validator->validate($clientCreationValues);
The $errors
format is like this:
$errors = [
'field_name' => [
'validation_rule_name' => 'Validation error message for that field',
'other_validation_rule_name' => 'Another validation error message for that field',
],
'first_name' => [
'minLength' => 'Minimum length is 3',
],
'email' => [
// Key was not present, requirePresence rule failed
'_required' => 'This field is required',
],
];
Validation error handling
Throwing the exception
Right after the validation is performed a ValidationException
is thrown if there are any errors
that can be caught in the middleware and transformed into a JSON response.
if ($errors) {
throw new ValidationException($errors);
}
The ValidationException
The ValidationException
is a custom exception that stores the validation error array
and may transform it into another format which is expected
by the frontend. This has the benefit of adding an abstraction layer between the
CakePHP Validation library error output and the frontend.
File: src/Domain/Validation/ValidationException.php
<?php
namespace App\Domain\Validation;
use RuntimeException;
class ValidationException extends RuntimeException
{
public readonly array $validationErrors;
public function __construct(array $validationErrors, string $message = 'Validation error')
{
parent::__construct($message);
$this->validationErrors = $this->transformCakephpValidationErrorsToOutputFormat($validationErrors);
}
/**
* Transform the validation error output from the library to array that is used by the frontend.
*
* Removes the rule name as keys and only keeps the error message.
*
* @param array $validationErrors The cakephp validation errors
*
* @return array the transformed result
*/
private function transformCakephpValidationErrorsToOutputFormat(array $validationErrors): array
{
$validationErrorsForOutput = [];
foreach ($validationErrors as $fieldName => $fieldErrors) {
// There may be cases with multiple error messages for a single field
foreach ($fieldErrors as $infringedRuleName => $infringedRuleMessage) {
// Removes the rule name as keys and replace with incremented numeric keys
$validationErrorsForOutput[$fieldName][] = $infringedRuleMessage;
}
}
return $validationErrorsForOutput;
}
}
Catching the exception and responding with JSON
The exception is caught in the ValidationExceptionMiddleware
that transforms
the errors into a JSON response with the appropriate status code and response body format.
File: src/Application/Middleware/ValidationExceptionMiddleware.php
<?php
namespace App\Application\Middleware;
use App\Application\Responder\JsonResponder;
use App\Domain\Validation\ValidationException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final readonly class ValidationExceptionMiddleware implements MiddlewareInterface
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
private JsonResponder $jsonResponder,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);
} catch (ValidationException $validationException) {
// Create response (status code and header are added later)
$response = $this->responseFactory->createResponse();
$responseData = [
'status' => 'error',
'message' => $validationException->getMessage(),
// The error format is already transformed to the format that the frontend expects in the exception.
'data' => ['errors' => $validationException->validationErrors],
];
return $this->jsonResponder->encodeAndAddToResponse($response, $responseData, 422);
}
}
}
This middleware is added at the end of the middleware stack before the ErrorHandlerMiddleware
.
File: config/middleware.php
return function (App $app) {
// ...
$app->add(\App\Application\Middleware\ValidationExceptionMiddleware::class);
$app->add(\App\Application\Middleware\ErrorHandlerMiddleware::class);
};
Testing validation
An example of a test for the validation of a user update request
can be found in the
Test Examples.