Skip to content

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:

->string('email', 'required', new DomainEmail('example.com'))

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:

'!notEmpty'   // value must be empty

This wraps the validator in Verja\Validator\Not, which inverts the result and swaps the error key (e.g. NOT_EMPTYIS_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:

->string('name', 'required', function ($value) {
    return ucwords(strtolower(trim($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.