Custom validators and filters¶
Implementing a validator¶
Implement Verja\ValidatorInterface or extend Verja\Validator (the base class handles
the error storage for you):
use Verja\Validator;
use Verja\Error;
class DomainEmail extends Validator
{
public function __construct(private string $domain) {}
public function validate($value, array $context = []): bool
{
if (!str_ends_with($value, '@' . $this->domain)) {
$this->error = new Error(
'WRONG_DOMAIN',
$value,
"value must be an email address at {$this->domain}",
['domain' => $this->domain]
);
return false;
}
return true;
}
}
Use it directly as an object:
Error object¶
new Error(
'ERROR_KEY', // machine-readable code (SCREAMING_SNAKE_CASE by convention)
$value, // the value that failed (always included in parameters as 'value')
'human message', // shown to developers/users
['extra' => ...] // additional parameters merged into Error->parameters
);
Return true from validate() on success, false on failure (and set $this->error).
Negating with !¶
Any validator can be negated with a ! prefix in string definitions:
This wraps the validator in Verja\Validator\Not, which inverts the result and swaps
the error key (e.g. NOT_EMPTY → IS_EMPTY, or more generally prepends/removes NOT_).
Implementing a filter¶
Implement Verja\FilterInterface or extend Verja\Filter:
use Verja\Filter;
class Slugify extends Filter
{
public function filter($value, array $context = []): mixed
{
return strtolower(preg_replace('/[^a-z0-9]+/i', '-', trim($value)));
}
}
If the value is unsuitable for filtering (wrong type, invalid format), throw
Verja\Exception\InvalidValue — this short-circuits the pipeline and adds an error:
use Verja\Filter;
use Verja\Gate;
use Verja\Exception\InvalidValue;
class JsonDecode extends Filter
{
public function filter($value, array $context = []): mixed
{
$decoded = json_decode($value, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$e = new InvalidValue('Invalid JSON');
$e->errors = [new \Verja\Error('INVALID_JSON', $value, 'value must be valid JSON')];
throw $e;
}
return $decoded;
}
}
Namespace registry¶
Register your namespace once and use string definitions everywhere:
// Register at bootstrap time
Verja\Validator::registerNamespace('App\\Validator');
Verja\Filter::registerNamespace('App\\Filter');
// Now use string definitions as usual
$gate = (new Gate())
->string('email', 'required', 'domainEmail:example.com')
->string('slug', 'required', 'slugify');
// ↑ App\Filter\Slugify
// ↑ App\Validator\DomainEmail with param 'example.com'
Namespaces are searched in LIFO order (last registered wins), so app namespaces take precedence over built-in ones. Class names are matched case-insensitively with ucfirst.
Multiple namespaces can be registered:
Verja\Validator::registerNamespace('App\\Validator');
Verja\Validator::registerNamespace('App\\Validator\\Specialized');
Callback shorthand¶
For one-off validations without a class:
use Verja\Error;
$gate = (new Gate())
->string('username', 'required', function ($value, array $context) use ($db) {
if ($db->exists('users', ['username' => $value])) {
return new Error('USERNAME_TAKEN', $value, 'username is already taken');
}
return true; // valid
});
A callable validator must return true on success or a Verja\Error on failure.
For filters, pass a callable that receives the value and returns the transformed value:
Whether a callable is treated as a filter or validator depends on the return type:
callables passed to addFilterOrValidator() are treated as validators. To explicitly add
as a filter, use addFilter($callable) on the gate directly.