RFC: New Validation Component

This Request for Comments proposes a new component targeting validation that
will eventually replace zend-validator, but initially complement it in order to
provide a stronger architecture, simplified maintenance, and more consistent
usage.

The component is yet to be named. When naming, we will be careful to ensure
there is no confusion between the names (e.g., we will not name it
zend-validation, as this is too close in naming to zend-validator, which
could lead to issues being raised in the wrong locations).

Why?

The zend-validator component has a number of short-comings that are due to past
architectural decisions. These include:

  • Soft dependencies on a number of other components, such as zend-cache,
    zend-session, zend-http, zend-uri, and more. Most of these are used by one or
    a subset of validators, but you need to pay attention to the Composer
    suggestions to know when and why you would need to install them. This makes
    working with zend-validator out-of-the-box difficult.

  • Stateful validators can lead to hard-to-debug issues. If the same instance of
    a validator is used in multiple validator chains (e.g., within an input
    filter), the value and messages retrieved from the validator will be based on
    when it was executed.

  • Difficult to understand constructors. Most validator constructors only take an
    array of options at this point, which requires that you know what option
    keys
    and their related values are expected. Because options may also be set
    after instantiation, you often cannot know if your validator is configured
    correctly until you execute it — which is generally too late.

  • Duplicated and verbose type-checking logic. Because the component was written
    before scalar type hints were added to PHP, and because most validator
    configuration was occuring via an options array, we ended up with a lot of
    conditionals to assert the validity of options values.

None of these short-comings are enough to address on their own, necessarily;
they are pain points, definitely, but nothing to break backwards compatibility
for.

Considering them as a whole, they indicate some very real maintenance and design
issues that if we address, could make usage easier.

Goals

  • Provide a migration path. zend-valdiator is used standalone and with
    zend-inputfilter in many, many places; we do not want to leave users with no
    path to upgrade. All of the following goals must address this point.

  • Upgrade to PHP 7.1. In particular, as we adapt existing validators to the new
    component, this will allow us to remove a ton of code that was doing type
    checking.

  • Extract component-specific validators to their own packages. This will mean
    additional packages that contain validators specific to a given component.
    Doing so ensures the new component has no hard or soft dependencies on these
    other components, nor do the components themselves have dependencies on the
    new component.

  • Remove unnecessary dependencies, or dependencies that may be inlined; examples
    include:

    • zend-filter (used in the Digits validator); since we use the defaults for
      that filter, we can inline the logic.
    • zend-config (only used in tests)
  • Stateless validation. All validators will return a validation result, which
    may be queried to determine outcome (“was validation successful?”), as well as
    to retrieve validation failure messages. As part of this approach, validators
    will no longer receive an array of options, but instead specific, typed
    constructor arguments. This will allow re-use of validators across multiple
    validation chains, and also reduce maintenance of individual validators.

    Additionally, this will extract the presentation of validation failure
    messages from the validators. Presentation logic currently handled includes
    value obfuscation, message translation, and message truncation; these can be
    moved into result decorators or helpers.

PHP 7.1 Upgrade

The PHP 7.1 upgrade allows a number of things:

  • Return type hints for most methods.
  • Scalar type hints for method arguments and return values.

Additionally, this means we can start using things such as the splat operator
with a new Callback validator implementation.

To make full use of typehints, we also need to write the constructors of each
validator such that they accept concrete arguments instead of an options array.
This will require that we create factories for each validator that accepts
options, and register those with the component’s plugin manager. Because
zend-servicemanager allows passing options when creating a service, we can write
these similar to the following:

use Psr\Container\ContainerInterface;
use RuntimeException;

class BetweenFactory
{
    public function __invoke(ContainerInterface $container, $serviceName, array $options = null)
    {
        $options   = $options ?: [];
        $min       = $options['min'] ?? 0;
        $max       = $options['max'] ?? PHP_INT_MAX;
        $inclusive = $options['inclusive'] ?? false;
        return new Between($min, $max, $inclusive);
    }
}

Using this approach, the options remain the same; the only difference is how the
plugin manager uses them to create new validator instances. This solves the 80%
case for migration; as direct instantiation is the minority use case.

Extracting Components and Dependency Reduction

We can reduce dependencies within the new component dramatically, making it
easier to use standalone, and bringing clarity to which validators have
additional requirements.

First, there are a number of components that are currently included in the
zend-validator composer.json that are never used, or only used in tests:

  • zend-cache (never used)
  • zend-config (used in tests; other solutions exist)

Second, there are components used by zend-validator, but where the functionality
could likely be inlined in order to reduce a dependency. These include:

  • zend-filter: the Digits validator uses a Digits filter with default
    values; this can be inlined.
  • zend-http: used by the bin/update_hostname_validator.php script; we could
    use curl instead.
  • zend-i18n: currently, this is to allow injecting validators with translators,
    if the translator service is available. Since translation will move to result
    objects, and those will likely move to a new component, this can be removed.

Third, there are a number of components included by zend-validator because
specific validators make use of them:

  • zend-db: used by the NoRecordExists and RecordExists validators
  • zend-math: used by the Csrf validator
  • zend-session: used by the Csrf validator
  • zend-uri: used by the Uri and Sitemap\Loc validators

For the first two sets of dependencies, we can omit them and adapt our code so
they are no longer necessary.

For the third set of dependencies, we have two options:

  • Split each set of validators into a separate package; e.g., zend-db-validator.
    This package then bridges the two components, and developers would install
    that package if they want those validators.
  • Write the validator functionality such that it no longer has dependencies.

The first option (splitting to a separate component) will be used for the
zend-db validators. This will also be used for the Csrf validator; while we
can replace its usage of Zend\Math\Rand::getBytes() with random_bytes(), it
still maintains a hard dependency on zend-session, and, as such, should be in a
separate package (likely zend-validator-csrf or zend-csrf-validator).

The second option will be used for the Uri and Sitemap\Loc validators (the
latter will be rewritten to use a Uri validator internally).

The composer.json package will suggest any extracted packages.

Stateless Validators

The following illustrates the problem in the current architecture:

$validator = new Between(['min' => 1, 'max' => 10, 'inclusive' => true]);

$chainOne = new ValidatorChain();
$chainOne->addValidator($validator);

$chainTwo = new ValidatorChain();
$chainTwo->addValidator($validator);

$chainTwo->isValid(11);
$chainOne->isValid(0);

$value    = $validator->getValue();
$messages = $validator->getMessages();

What values are expected? The answer is: it depends on the order in which
the two validator chains are executed. In this case, the value will be 0, and
we will have messages related to that; if we reversed the order in which the two
chains are executed, we’ll get different values.

This may seem unlikely, but consider:

  • Validating a form, where you might have a collection of items that have the
    same validation. If you use the same chains between them, and multiple
    collections fail validation, the messages will not match the given collection.
  • zend-inputfilter uses validator chains internally, and can suffer from the
    same problem.

Additionally, all validators currently define public setter methods that allow
you to change the various constraints the validator uses during validation. As
such, if you were to share an instance, but change a constraint to suit a
particular validator chain, all places that instance is used will have the
changed constraint — which likely is not desired.

These problem forced us to not share validator instances returned by the plugin
manager, but can still be encountered if you register instances manually.

The solution is to make validators stateless. By this, I mean specifically:

  • Values required to allow the validator to work cannot be changed after
    instantiation. As such, these can and should be typed, and directly
    injected in the constructor
    . This goes hand-in-hand with the proposed updates
    to PHP 7.1.

  • Validation should return a result of validation. Users would query this
    result instance to determine if validation was successful, and pull any
    validation failure messages from it. It would also compose the value, to allow
    access to that by a result consumer.

As such, I propose the following:

  • A final ValidationFailureMessage class defining a value object to contain a
    message template and any variables that might be substituted into the
    template.
  • A Result interface defining methods for querying validation status and
    retrieving an array of ValidationFailureMessage instances; this would
    represent a single validation result.
  • A final ValidatorResult class implementing the Result interface. This
    would be the only implementation shipped.
  • Adding a Validator interface that defines a single method,
    validate($value, $context = null) : ValidatorResult.

We would also provide a helper class for rendering ValidationFailureMessage
instances
. This would simply interpolate variables into the message templates
and spit them out. One or more separate packages could then provide helpers that
provide features such as message translation, message truncation, and
value obfuscation.

Each validator would be updated to implement the new interface. A new
AbstractValidator implementation would be stripped of all message presentation
logic, storing only message templates and message variables to include in
ValidationFailureMessage instances it returns in a ValidatorResult.

The new ValidatorChain implementation would implement Validator, and only
accept Validator instances. During validation, it would create a
ValidatorResult that contains the final $isValid (based on all elements in
the chain), and, if invalid, an aggregate of all ValidationFailureMessage
instances from all validators that failed.

All interfaces and proposed classes are listed in their entierty below in the
Appendix “Interfaces, Traits, and Classes”.

Compatibility concerns

The proposed package will initially depend on zend-validator v2, and provide
the proposed interface, classes, and traits only, with one or two existing
validators re-written to demonstrate how to write validators under the new
architecture.

We will ship a trait that adapts a given zend-validator ValidatorInterface
implementation so that it can also work as a Validator implementation; a
suggested implementation, LegacyValidator, is provided in the Appendix
"Interfaces, Traits, and Classes". This will allow users to use existing
validators, but then gradually rewrite them to work under the new architecture.

But what about…?

Logging

When logging, you often do not need validation failure message strings, but
rather their codes and the values that belong to each.
ValidationFailureMessage, for this reason, composes a code related to the
message, as well as the associated values (which are often used to form the
final message for views as well). A logger might then serialize these values
using JSON:

$logger->warn(json_encode([
    'code'    => $message->getCode(),
    'context' => $message->getVariables(),
]));

Translation

Many developers have indicated that translation is a perk of the current
zend-validator implementation, and something that should be turn-key.

We feel this can be addressed easily with helper classes that accept either a
result or the array of validation failure message instances. As an example:

use Zend\I18n\Translator\TranslatorInterface;
use Zend\YetToBeNamed\Result;
use Zend\YetToBeNamed\ValidationFailureMessage;

class ResultMessageTranslator
{
    private $textDomain;
    private $translator;
    
    public function __construct(TranslatorInterface $translator, string $textDomain)
    {
        $this->translator = $translator;
        $this->textDomain = $textDomain;
    }

    public function __invoke(Result $result) : array
    {
        if ($result->isValid()) {
            return [];
        }

        $messages = [];
        foreach ($result->getMessages() as $message) {
            $messages[] = $this->prepareMessage($message);
        }
        return $messages;
    }

    private function prepareMessage(ValidationFailureMessage $message)
    {
        $translated = $this->translator->translate($message->getTemplate(), $this->textDomain);
        foreach ($message->getVariables() as $key => $substitution) {
            $pattern = sprintf('%%%s%%', $key);
            $translated = str_replace($pattern, $substition, $translated);
        }
        return $translated;
    }
}

We will likely provide a separate package that does exactly this, which would
make this then a turn-key solution for developers.

Appendix

Interfaces, Traits, and Classes

Below are the proposed interfaces, traits, and classes for the new component.

Class: ValidationFailureMessage

final class ValidationFailureMessage
{
    /** @var string */
    private $code;

    /** @var string */
    private $template;

    /** @var array */
    private $variables;

    public function __construct(string $code, string $template, array $variables = [])
    {
        $this->code = $code;
        $this->template = $template;
        $this->variables = $variables;
    }

    public function getCode() : string
    {
        return $this->code;
    }

    public function getTemplate() : string
    {
        return $this->template;
    }

    public function getVariables() : array
    {
        return $this->variables;
    }
}

Interface: Result

interface Result
{
    public function isValid() : bool;

    /**
     * @return ValidationFailureMessage[]
     */
    public function getMessages() : array;

    /**
     * @return mixed
     */
    public function getValue();
}

Class: ValidatorResult

final class ValidatorResult implements Result
{
    /** @var bool */
    private $isValid;

    /** ValidationFailureMessage[] */
    private $messages;

    /** @var mixed */
    private $value;

    public function __construct(bool $isValid, $value, array $messages = [])
    {
        $this->isValid = $isValid;
        $this->value = $value;

        array_walk($messages, function ($message) {
            if (! $message instanceof ValidationFailureMessage) {
                throw new InvalidArgumentException(sprintf(
                    'All validation failure messages must be of type %s; received %s',
                    ValidationFailureMessage::class,
                    is_object($message) ? get_class($message) : gettype($message)
                ));
            }
        })
        $this->messages = $messages;
    }

    public function isValid() : bool
    {
        return $this->isValid;
    }

    /**
     * @return ValidationFailureMessage[]
     */
    public function getMessages() : array
    {
        return $this->messages;
    }

    /**
     * @return mixed
     */
    public function getValue()
    {
        return $this->value;
    }
}

Interface: Validator

interface Validator
{
    public function validate($value, $context = null) : ValidatorResult;
}

Trait: LegacyValidator

trait LegacyValidator
{
    public function validate($value, $context = null) : ValidatorResult
    {
        if ($this->isValid($value, $context)) {
            return new ValidatorResult(true, $value);
        }

        $messageVariables = array_merge(
            $this->abstractOptions['messageVariables'],
            ['value' => $value]
        );

        $messages = [];
        foreach (array_keys($this->abstractOptions['messages']) as $messageKey) {
            $template = $this->abstractOptions['messageTemplates'][$messageKey];
            $messages[] = new ValidationFailureMessage($template, $messageVariables);
        }
        return new ValidatorResult(false, $value, $messages);
    }
}

Usage would be something like:

class CustomValidator extends AbstractValidator implements
    Validator,
    ValidatorInterface
{
    use LegacyValidator;

    /* ... */
}

Existing Proposals

I have reviewed the following proposals which predate this one. Each has some
unique ideas; none offers a complete roadmap, however.

“Road to ZF3”

Suggests the following:

  • Moving component-specific validators (e.g., Barcode, Db, File, etc.) to their
    specific components and deprecating them within zend-validator.
  • Stateless validators: validators return a result.

ValidationResult

Implements a ValidatorResultInterface, and modifies ValidatorInterface to
define validate() (instead of isValid(); the method now takes an optional
$context = null argument.

Validator result interface proposal

This proposal simply proposes new interfaces, without implementation:

  • Defines a ResultInterface, with isValid(), isNotValid(),
    getErrorCodes(), and getMessages() methods.
  • Defines a TranslatableMessageInterface, with getMessageTemplate(),
    getMessageVariables(), getTranslationDomain(), and __toString() methods.

v3 proposal

Extends the previous proposal, and adds PHP 7.1 support. The specific extensions
include:

  • A final Message value object, composing a message key, message template, and
    message variables.
  • A final Result value object, composing the results of validation, and an
    array of Message instances.
  • An update to ValidatorInterface to rename isValid() to validate(), and
    have it return a Result.

Refactor to stateless validators

Independent proposal based on online discussions. Includes:

  • Result and ResultAggregate interfaces, with default implementations, as
    well as examples demonstrating decoration.
  • Validator interface.
  • Updates to AbstractValidator and ValidatorChain
6 Likes

Can you please show how this will look? Will each validator then need a factory?

Arrays of options are elsewhere in ZF3 - configs and forms come to mind. Will this new way make validator an oddity within the ZF3 universe, and therefore harder for beginners to grok?

Yes, they’ll each need a factory; this is noted in the proposal.

In terms of options, what this approach does is two-fold:

  • When directly instantiating a validator, you would pass the specific constructor arguments.
  • When pulling the validator from a plugin manager — as you typically do with the validator chain or when using zend-inputfilter — you would still pass the options array; the factories would then map those to constructor arguments.

As such, the primary use cases — usage with validator chains, input filters, and forms — remains unchanged.

Hey Matthew,

First of all, the entire proposal is amazing: I always wanted to have validators (and hopefully, in the far future, Forms) as immutable, final (the inheritance in our validators is a mess) and stateless.

Let me comment about a few problematic sections:

A final ValidationFailureMessage class defining a value object to contain a
message template and any variables that might be substituted into the
template; it would also contain a __toString() method that would interpolate
the variables into the message template, when no further transformations are
required.

This hit me before: we need two value objects to clarify intent. One must be the before-translation-error-messages, the other one would be post-translation-error-messages. This separation makes it clear to the user whether the messages are yet to be translated or not. Do not attempt to perform the translation inside ValidationFailureMessage. Do not attempt to put logic in __toString().

A final ValidatorResult class defining a value object representing a
single validation result. It would define a method for querying validation
status, the value validated, and any validation failure messages (represented
by FailureMessage instances).

May I suggest ValidationResult? Also: can we ensure that ValidationResult is immutable and serializable? This would allow simple auditing of validation attempts (security concern, if somebody attempts to bruteforce an endpoint, for example), or could be used to tell the user in detail about what happened. This is tricky, as it requires the input data to be immutable too, so that’s a discussion that could eventually go nowhere.

Adding a Validator interface that defines a single method,
validate($value, $context = null) : ValidatorResult.

Can we haz __invoke(), so we can use functional composition patterns? :slight_smile:
Also, $context = null has always been a hack - can we finally formalise context, so we can later build an API on it, such as $context->getParent() or similar? This has always been problematic for anyone creating nested validators, where a child value validation depends on parent data.

Value obfuscation, message truncation, and message translation would be left to
the user; these would consume ValidationFailureMessage instances, pulling the
template and variable substitutions from them, and returning a string message.
(Alternately, they could decorate the instance and implement a __toString()
method.)

Do not push the concern of rendering the messages into the ValidationFailureMessage object itself - use view helpers, services, translators or similar, I’d say. Maybe visitor pattern (although risky performance slippery slope), but __toString() is generally a no-go.

The problem now is just on how to approach the BC break. Zend\Validator is so huge that IMO this new API should be a new namespace or even library - no mixing up allowed.

1 Like

The way that I’ve defined that class above, we can assume that this is pre-translation. The class is final, and only contains message templates and the variable substitutions for use with that message.

The main reason I included __toString() is because the 80% use case will be not to translate the message, but instead just use it directly; as such the __toString() implementation provides a way to substitute any variables into the message template so you can have a readable string representation.

If we do not do that, we then need some sort of utility class for “rendering” the message, making usage much more difficult for the most common case. Considering a growing use case is reporting errors for APIs, I don’t want to make that harder than it needs to be.

So, what I’m suggesting is that users should be able to do something like this:

return $problemDetails->createResponse(
    $request,
    422,
    'Submitted book failed validation',
    'Invalid book',
    'https://api.example.com/types/book/invalid-book',
    [ 'validation_messages' => $result->getMessages() ]
);

Since the objects are serializable as strings, that would give the following:

"validation_messages": [
    "Published date was out of range; expected minimum date of 1900-01-01",
 ]

If we cannot do this, we need to have some sort of solution for serializing messages without translation. Would a class with a static method do? Or do you have some other suggestion?

There are some problems to be considered here:

  1. __toString() cannot perform I/O (translation counts as I/O), since __toString() is PHP’s only function where no exceptions are allowed (HAHA!). Anything more complex than just dumping the values is a no-go here, and we’ll hit that really fast anyway.
  2. some values need to be hidden. An Identical validator used for password comparison should probably never store the actual validated value inside the ValidationFailureMessage due to security issues arising from that. Interpolating the password in the error message (by accident) would be a security disaster, IMO. The valid/invalid values should probably never make it into the ValidationFailureMessage either.
  3. the distinction of ValidationFailureMessage and TranslatedValidationFailureMessage (not in the same inheritance tree, nor sharing interfaces) is mostly for separating the two at type level, avoiding passing around technical information as user-facing information. This way, we can use technical key/values in the ValidationFailureMessage, without ruining the UX with cryptic/laughable error messages
  4. $translateErrorMessages($result, $result->getErrorMessages()) or similar would be sufficient, where $translateErrorMessages is the translator service. The ValidationFailureMessage and TranslatedValidationFailureMessage could be JsonSerializable or similar too, to reduce API bloat.

Taking your example from above, the following could print the message templates, or maybe just the error identifiers:

return $problemDetails->createResponse(
    $request,
    422,
    'Submitted book failed validation',
    'Invalid book',
    'https://api.example.com/types/book/invalid-book',
    // produces technical information, such as the string "Zend\Validator\StringLength::E_INVALID_STRING_LENGTH"
    [ 'validation_messages' => $result->getMessages() ]
);

This would print human-friendly information:

return $problemDetails->createResponse(
    $request,
    422,
    'Submitted book failed validation',
    'Invalid book',
    'https://api.example.com/types/book/invalid-book',
    // produces `TranslatedValidationFailureMessage`, "The string 'foo' is too short, expected a minimum length of 5 characters"
    [ 'validation_messages' => $translateErrors($result) ]
);

The serialization itself is not a big deal - JsonSerializable will be mostly sufficient, as well as typical toArray(), getMessageKeys(), etc. The transformation, the hiding of information and the interpolation are a bit more troublesome as per description above.

I don’t understand the benefit for end-users of changing the way validators are constructed. Array of options are a great way of implementing named parameters. It avoids the mess of trying to specify 1 or 2 values among too many parameters with default values. And I believe validators are classes where quite a few parameters might be needed in constructor.

The suggested solution, using a dedicated factory for each validator, does not change the usage for 80% of usage (according to you), but it will break 20% of code that will now need to deal with a long list of parameters for no perceived benefits. The only benefit you mention is that the end-user will not have to guess possible keys for options. But in fact this will still be needed for 80% of users that will use the factories, isn’t it ?

To me that sounds like over-engineering. Making something works differently than a de-facto standard within ZF (array of options), breaking compatibility with no benefit for end-user (but disadvantage for 20% of them) and making the code more complex by introducing factories where they might not be needed, does not sound like an improvement to me.

IMHO, the proposed changes are very beneficial. Working with the rather obscure bits in the current Validator model are frustrating. Learning about $context is certainly non-obvious at best. Factories will help smooth over a transition and cover that 80% (100% for my use-cases). Stronger type enforcement at the beginning is always a win. Immutability and statelessness are huge in my book.

In contrast to your comment about this being over-engineered … this seems like the appropriate amount of engineering to the problem at hand based on working with Validator here and there over the years. By creating explicit requirements within the actual validation classes and clear expectations of what’s needed helps to make the validators both more portable as well as provide generous type-hints that work across IDEs leading to a better development flow.

I’d much rather have my application barf and die saying that I’m missing argument four that should be of type SomeRequiredDependency than just a mysterious failure on nestedLine137ThatReferencesSomeOtherProtectedVariable within the class.

Fair enough.

Okay, so this sounds like two constraints:

  • Never interopolate the submitted value within a ValidationFailureMessage. I think that’s pretty easy for us to accomplish, and mirrors most of what I’ve seen in the current set of validators (most do not reference %value% anymore, and the remainder can be easily changed not to do so.
  • Never interpolate the conditions in a ValidationFailureMessage. This I’m not sure I agree with: one aspect of a good validation failure message is that it details the conditions necessary to validate. If a value should be between 1 and 10, and all I get is a message stating “The value is out of range”, I have no idea how to correct it: should I be providing a smaller number? a larger one? etc.

Can you clarify, please? I am totally behind the first point, but the second makes no sense to me.

To clarify: $translateErrorMessages would have a signature like the following:

/**
 * @return string[] Each item would be a string containing the translated
 *     message, with values interpolated.
 */
function (ValidationResult $result) : array;

Is this correct?

When serializing, what would the structure look like here? Array of strings,
with each having an interpolated message? or something else?

You show in your example that the raw version might include technical
information, such as Zend\Validator\StringLength::E_INVALID_STRING_LENGTH,
but, again, this speaks to the issue I present above: I have no idea how to
correct the input at this time. Is the string too short? too long? what would be
valid?

If your idea is that ValidationFailureMessage would only provide technical
details out-of-the-box, and a formatter of some sort would be necessary to
provide interpolated strings, why, exactly? This seems more complex for the most
standard use cases (providing useful error information for either forms or API
consumers).

Just trying to wrap my head around the feedback.

This looks great.

Regarding the Csrf validator. Could it consume a Zend\Validator\Csrf\StorageInterface (or similar) instead of Zend\Session\Container? First this would help eliminate a dependency. It would also enable developers to store the tokens somewhere other than in session. My use case is for an ajax-heavy application that’s using lock-less sessions for performance. To avoid the CSRFs getting clobbered by overlapping requests I’m storing them using SimpleCache, but to do that I had to write my own Csrf validator (inheriting much from the clipboard :wink: )

Could be as simple as:

<?php

namespace Zend\Validator\Csrf;

interface StorageInterface
{
    /**
     * Sets name (collision-avoidance)
     */
    public function setName(string $name);

    /**
     * Returns hash from storage
     */
    public function get(string $token) : string;
    
    /**
     * Store hash in storage
     */
    public function set(string $token, string $hash);
    
    /**
     * Removes token from storage
     */
    public function delete(string $token);
}

…with maybe a zend-validator-csrf-session package for the majority who are happy with the existing setup.

I think that’s a great idea; I’ll make note of that now!

I’ve audited the various validators, and the majority take between 1 and 4 parameters. Of those that take more than 1, the majority have only 1 or 2 parameters that are truly optional. Validators that have multiple optional parameters are outliers within the library.

As noted earlier, too: the typical use case for creating validators is via a plugin manager or some builder that takes configuration to pass to the plugin manager. This means you have the option of providing an array of options just as you do now. Manual usage is relatively uncommon with this library; I suspect this is in part due to needing to memorize option keys, which are not documented anywhere, and which require looking at the code. Having actual constructor arguments allows users to benefit from IDE autocompletion and hinting, which should actually improve manual usage for developers.

I totally get the affinity for passing an array of options; I’ve long wanted the ability to pass named arguments in PHP. However, I think this is a case where they do not provide as much benefit as we may think.

The translation ZF provides out-of-the-box is useful, I am very open and happy to change all my codebase to adapt to a better system, but don’t force the user to create a tool that do something that the framework did automatically before. A specific part of the library with a dedicated upgrade doc should be provided for message translation too.

Consistency is a big perk of ZF. I’m totally against abandoning the naming convention of XxxInterface, AbstractYyy and ZzzTrait

I see a lot of benefits in the way @matthew presented the change:

  1. As he said, you are going to use them exactly like now
  2. Validators are going to be much more easily tested and maintained, and this affect users too in the way bugs are less likely to happen and contributing become easier
  3. Understanding a validator through a factory that translates array-config to constructor parameters is easier than reading the source code of the constructor in v2

Following what already done in ZF, I vote for zend-validator-csrf and zend-validator-db

I have just made a few significant updates to this proposal; the highlights include:

  • The proposal now is for a new component, and not a new version of zend-validator. This provides a cleaner migration path, as users can install both side-by-side, and we do not need to maintain new interfaces in a v2 release and legacy interfaces in a v3 release.

  • The proposal now adds a Result interface, which ValidatorResult implements. This will allow for decorators.

  • ValidationFailureMessage now also composes a $code, which will allow contexts where string messages are not of interest when reporting errors (e.g., logging).

  • I’ve added a new section, “But what about…?” to cover topics that have arisen in comments or other discussions.

One other idea I will be looking at is expanding this new component to cover the features of zend-inputfilter, as we also want to apply the idea of statelessness to that component. Considering that the majority aspect of zend-inputfilter is to address validation, several of us feel that this approach could reduce some redundancy between the two components.

The proposed package will initially depend on zend-validator v2, and provide
the proposed interface, classes, and traits only, with one or two existing
validators re-written to demonstrate how to write validators under the new
architecture.

I would prefer that we didn’t depend on zend-validator v2 as we’re committing to supporting every validator’s options. Are there really so many validators that we can’t get them written with a “help-wanted” label and advertising? Though maybe you’ve audited this and that commitment is not a problem?

@akrabat — Depending on zend-validator v2 would allow us to ship a plugin manager that adds delegators to existing zend-validator implementations in order to return instances decorated in a LegacyValidator, which could be interesting in terms of migrations.

Alternately, we could do that aspect (the LegacyValidator implementation, and the alternate plugin manager and/or plugin manager configuration) via a separate “migration” package. If we did, however, that package would need a new release each time we ship a new release of the new component in order to remove validators that have been migrated.

I could go either way, to be honest.

As discussed today, I’d also suggest not depending on zend-validator for a start, and instead have a dummy “callback validator” that we can use to see if the component/architecture is usable/useful.

Plugging in all existing validators could be as simple as requiring a package providing plugin manager config.

Yes, this means even more packages, but we can integrate/hardcode them into the new component once we know that the solution works.

I’m sorry, Am I the only one dazed by the drastic turn in the naming convention that removes Interface suffix from the interfaces proposed?

Likely this was discussed and accepted in another RFC or topic and I missed it, if so I would appreciate a link to it.

If not, I feel the need to bring the attention to this point too: could we stick to ZF2 naming convention?

1 Like

@Slamdunk We haven’t made or communicated a decision yet. However, all the latest RFCs have omitted the Interface suffix, including the now published zend-expressive-hal and zend-problem-details packages.

My own rationale for doing so is nicely summed up in this article from Mathias Verraes, where he argues that an interface, as the contract you are programming to, is a first-class citizen of your code. Implementations should indicate how they do so, or what context they do so for. While I’ve long been a proponent of the Interface suffix, when I consider developing the interface first, based on the use cases we need to accommodate, I find I agree with Mathias more and more.