Validation
Overview
Tempest provides a Validator object capable of validating an array of values against the public properties of a class or an array of validation rules.
While validation and data mapping often work together, the two are separate components and can also be used separately.
Validating against objects
When you have raw data and an associated model or data transfer object, you may use the validateValuesForClass() method on the Validator. Note that the validator needs to be resolved from the container.
$failingRules = $this->validator->validateValuesForClass(Book::class, [ 'title' => 'Timeline Taxi', 'description' => 'My sci-fi novel', 'publishedAt' => '2024-10-01', ]);
This method accepts a fully-qualified class name as the first argument, and an array of data as the second. The values of the data array will be validated against the public properties of the class.
In this case, validation works by inferring validation rules from the built-in PHP types. In the example above, the Book class has the following public properties:
use Tempest\DateTime\DateTime; final class Book { public string $title; public string $description; public ?DateTime $publishedAt = null; }
If validation fails, validateValuesForClass() returns a list of fields and their respective failed rules.
Adding more rules
Most of the time, the built-in PHP types will not be enough to fully validate your data. You may then add validation attributes to the model or data transfer object.
use Tempest\Validation\Rules; final class Book { #[Rules\HasLength(min: 5, max: 50)] public string $title; #[Rules\IsNotEmptyString] public string $description; #[Rules\HasDateTimeFormat('Y-m-d')] public ?DateTime $publishedAt = null; }
Skipping validation
You may have situations where you don't want specific properties on a model to be validated. In this case, you may use the #[SkipValidation] attribute to prevent them from being validated.
use Tempest\Validation\SkipValidation; final class Book { #[SkipValidation] public string $title; }
Validating an existing object instance
When you already have an instantiated object, you may use the validateObject() method. Unlike validateValuesForClass(), this method takes an object instance and reads the actual values of its public properties directly.
$config = $this->configRepository->find($id); $this->validator->validateObject($config);
This is particularly useful where an object carries validation rules as part of its invariants, and you want to enforce them after the object has been hydrated from a source.
Behavior differences from validateValuesForClass()
There are a few important differences to be aware of:
It throws instead of returning. When validation fails, validateObject() throws a ValidationFailed exception rather than returning a list of failures.
Uninitialized properties are skipped. If a public property has not been initialized on the object, it is silently skipped.
`#SkipValidation` is not respected. Unlike validateValuesForClass(), this method does not check for the #[SkipValidation] attribute: all initialized public properties are validated.
Nested objects are not recursed into. If a property holds another object, that nested object's properties are not validated.
Validating against specific rules
If you don't have a model or data transfer object to validate data against, you may alternatively use the validateValues() and provide an array of rules.
$this->validator->validateValues([ 'name' => 'Jon Doe', 'email' => 'jon@doe.co', 'age' => 25, ], [ 'name' => [new IsString(), new IsNotNull()], 'email' => [new IsEmail()], 'age' => [new IsInteger(), new IsNotNull()], ]);
If validation fails, validateValues() returns a list of fields and their respective failing rules.
Validating a single value
You may validate a single value against a set of rules using the validateValue() method.
$this->validator->validateValue('jon@doe.co', [new IsEmail()]);
Alternatively, you may provide a closure for validation. The closure should return true if validation passes, or false otherwise. You may also return a string to specify the validation failure message.
$this->validator->validateValue('jon@doe.co', function (mixed $value) { return str_contains($value, '@'); });
Accessing error messages
When validation fails, a list of fields and their respective failing rules is returned. You may call the getErrorMessage method on the validator to get a localized validation message.
use Tempest\Support\Arr; use Tempest\Validation\Rules\IsEmail; // Validate some value $failures = $this->validator->validateValue('jon@doe.co', new IsEmail()); // Map failures to their message $errors = Arr\map_iterable($failures, fn (FailingRule $failure) => $this->validator->getErrorMessage($failure));
You may also specify the field name of the validation failure to get a localized message for that field.
$this->validator->getErrorMessage($failure, 'email'); // => 'Email must be a valid email address'
Overriding translation messages
You may override the default validation messages by adding a translation file anywhere in your codebase. Note that Tempest uses the MessageFormat 2.0 format for localization.
validation_error: is_email: | .input {$field :string} {$field} must be a valid email address.
Sometimes though, you may want to have a specific error message for a rule, without overriding the default translation message for that rule.
This can be done by using the #[TranslationKey] attribute on the property being validated. For instance, you may have the following object:
final class Book { #[Rules\HasLength(min: 5, max: 50)] #[TranslationKey('book_management.book_title')] public string $title; }
When this rule fails, the getErrorMessage() method from the validator will use validation_error.has_length.book_management.book_title as the translation key, instead of validation_error.has_length.