Migration

Composing final Validators

In version 3.0, nearly all validators have been marked as final.

This document aims to provide guidance on composing validators to achieve the same results as inheritance may have.

Consider the following custom validator. It ensures the value given is a valid email address and that it is an email address from gmail.com

namespace My;

use Laminas\Validator\EmailAddress;

class GMailOnly extends EmailAddress
{
    public const NOT_GMAIL = 'notGmail';

    protected $messageTemplates = [
        self::INVALID            => "Invalid type given. String expected",
        self::INVALID_FORMAT     => "The input is not a valid email address. Use the basic format local-part@hostname",
        self::INVALID_HOSTNAME   => "'%hostname%' is not a valid hostname for the email address",
        self::INVALID_MX_RECORD  => "'%hostname%' does not appear to have any valid MX or A records for the email address",
        self::INVALID_SEGMENT    => "'%hostname%' is not in a routable network segment. The email address should not be resolved from public network",
        self::DOT_ATOM           => "'%localPart%' can not be matched against dot-atom format",
        self::QUOTED_STRING      => "'%localPart%' can not be matched against quoted-string format",
        self::INVALID_LOCAL_PART => "'%localPart%' is not a valid local part for the email address",
        self::LENGTH_EXCEEDED    => "The input exceeds the allowed length",
        // And the one new constant introduced:
        self::NOT_GMAIL => 'Please use a gmail address',
    ]; 

    public function isValid(mixed $value) : bool
    {
        if (! parent::isValid($value)) {
            return false;
        }

        if (! preg_match('/@gmail\.com$/', $value)) {
            $this->error(self::NOT_GMAIL);

            return false;
        }

        return true;
    }
}

A better approach could be to use a validator chain:

use Laminas\Validator\EmailAddress;
use Laminas\Validator\Regex;
use Laminas\Validator\ValidatorChain;

$chain = new ValidatorChain();
$chain->attachByName(EmailAddress::class);
$chain->attachByName(Regex::class, [
    'pattern' => '/@gmail\.com$/',
    'messages' => [
        Regex::NOT_MATCH => 'Please use a gmail.com address',
    ],
]);

Or, to compose the email validator into a concrete class:

namespace My;

use Laminas\Validator\AbstractValidator;
use Laminas\Validator\EmailAddress;

final class GMailOnly extends AbstractValidator
{
    public const NOT_GMAIL = 'notGmail';
    public const INVALID = 'invalid';

    protected $messageTemplates = [
        self::INVALID   => 'Please provide a valid email address',
        self::NOT_GMAIL => 'Please use a gmail address',
    ];

    public function __construct(
        private readonly EmailAddress $emailValidator
    ) {
    }

    public function isValid(mixed $value) : bool
    {
        if (! $this->emailValidator->isValid($value)) {
            $this->error(self::INVALID);

            return false;
        }

        if (strtoupper($value) !== $value) {
            $this->error(self::NOT_GMAIL);

            return false;
        }

        return true;
    }
}

In the latter case you would need to define factory for your validator which for this contrived example would seem like overkill, but for more real-world use cases a factory is likely employed already:

use Laminas\Validator\EmailAddress;
use Laminas\Validator\ValidatorPluginManager;
use Psr\Container\ContainerInterface;

final class GMailOnlyFactory {
    public function __invoke(ContainerInterface $container, string $name, array $options = []): GMailOnly
    {
        $pluginManager = $container->get(ValidatorPluginManager::class);

        return new GmailOnly(
            $pluginManager->build(EmailAddress::class, $options),
        );
    }
}