Skip to content
This repository has been archived by the owner on Jan 31, 2020. It is now read-only.

[WIP] Refactor to stateless validators #181

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from

Conversation

weierophinney
Copy link
Member

This work-in-progress is exploring how we might approach stateless validators.

Essentially, instead of an isValid() method returning a boolean, and a subsequent call on the validator to retrieve validation error messages, we would instead:

  • Define a Result interface modeling the results of validation; it would compose the value validated, validation status, and, if present, any validation error messages.
  • Define validators would define a single validate() method, accepting both a value and optional context, and return a Result instance.
  • Define a ResultAggregate interface for aggregating several results, as is necessary in a ValidatorChain; Result instances would be pushed upon an aggregate.

Translation, value obscuration, and message truncation then become presentation issues, and can be achieved by decorating Result instances.

Additionally, we'd remove the concept of $options for creating individual validator instances; they would instead have concrete constructor arguments. This simplifies a ton of logic internally, but means that each validator would require its own factory class. On the flip side, it also means developers can write factories for specific options combinations, and have shared instances without worrying about state.

Migration concerns

We could create a new minor release that adds the new Validator, Result, and ResultAggregate interfaces, and various Result implementations. Validator would define validate instead of isValid(), allowing devs to implement both in order to forward-proof their validators. We could also provide a wrapper for Validator implementations to make them work as ValidatorInterface instances; in such a case, it would pull info from the result in order to populate its members.

The bigger issue is existing validators, and extensions to them. Developers extending existing validators may want to copy/paste implementations and begin migration of those to make them forwards-compatible with version 3. Since we would have version 3 released simultaneously to a v2 with the new interface additions, they could even copy those from v3 to aid their migration.

Integration concerns

I have not yet investigated impact on zend-inputfilter; however, that version will require a similar refactor towards stateless inputs/input filters as well.

Instead of `isValid() : bool`, `isValid()` now returns a
`ValidatorResult`, which will contain the value validated, results of
validation, and, in the case of an invalid result, validation error
messages.

Additionally, `isValid()` now accepts an additional, optional array
argument, `$context`; this allows validators to check against other
values in a validation set in order to do their work.
`ValidatorResult` now composes message templates and message variables
instead of messages; this will allow the various validators to omit
translation features.

A new class, `ValidatorResultTranslator`, composes a translator and text
domain, and accepts a `ValidatorResult` to a method
`translateMessages()`; it will then return an array of translated
message strings.

`ValidatorResult::getMessages()` will return the verbatim message
templates with variable interpolations, with no translation.
Removes all translator awareness from the `AbstractValidator`.

Consolidates `error` and `createMessage` to only do a lookup for the
given message key; `error` is removed.

Removes `__get()` and `__invoke()` as being superfluous.

Removes memoization of error messages and the validated value.
Realized that decoration is likely the best path for altering the values
returned via `getMessages()`. To enable that, added a new interface,
`Result`, describing the capabilities of a validation result, and
modified `ValidatorResult` to implement it.

Renamed `ValidatorResultTranslator` to `TranslatableValidatorResult`; it
now accepts a `Result` to its constructor and implements `Result`. For
all methods other than `getMessages()`, it proxies to the underlying
`Result` instance (these are implemented by a new trait,
`ValidatorResultDecorator`).

Also provides a new result type, `ObscuredValueValidatorResult`; it
overrides the `getValue()` method to obscure the value, and the
`getMessages()` method to ensure that its own `getValue()` is called
when performing message template substitutions.

A new method was extracted within `ValidatorResultMessageInterpolator`,
`castValueToString()`; this was done to allow the
`ObscuredValueValidatorResult` to work with the string value when
performing obfuscation.
- Removing "Interface", "Trait" suffixes as part of PHP 7.1 initiative.
Adds `AbstractValidator::createInvalidResult()`, for automating creation
of the `Result` instance returned by validators; that method retrieves
method templates based on the message keys passed to it, and pulls the
message variables from the instance, passing them along with the value
to the `ValidatorResult::createInvalidResult()` constructor.

If the "value obscured" flag is enabled, decorates the result with
`ObscuredValueValidatorResult` before returning it.

The `Between` validator was refactored to use this approach.
Updates the `Between` validator tests to reflect the refactor to return
Result instances.

Discovers a bug in the ValidatorResult::getMessageTemplates method, and
corrects it.
Done for two reasons. First, the method no longer returns a boolean, so
calling it `isValid()` did not indicate that there might be a
non-boolean result. Second, having the name different will allow us to
add the `Validator` interface to v2 releases, and thus provide a
forwards-compat path for developers.
Validator chains need to aggregate results... but still return a
`Result`. This commit does the following:

- Adds a `ResultAggregate` interface, which extends `Result`, as well as
  `IteratorAggregate` and `Countable`. It defines one additional method,
  `push()`, allowing pushing validation results onto the aggregate.
- Adds `ValidatorResultAggregate` as the only concrete implementation of
  a `ResultAggregate`.
- `ValidatorChain` now implements `Validator`, and its `validate()`
  method creates a `ValidatorResultAggregate` and pushes the result of
  each validator in the chain to it.
- `TranslatableValidatorResult` and `ObscuredValueValidatorResult` now
  check if the underlying result is an aggregate when `getMessages()` is
  called; if so, they loop through each result in order to aggregate
  messages to return. This allows them to act strictly as decorators.
Like translation, value obscuration is a presentation issue; leave it to
the decorators.
Like translation and value obfuscation, message truncation is a
presentation issue.
Actual failure messages are no longer stored in the validator, only
message templates and variables.
…act options to instance variables

Removes the constructor, `getOptions()`, `setOptions()`, and
`setOption()` from the `AbstractValidator`. Additionally, it promotes
the two remaining members of `$abstractOptions` to instance variables,
and modifies the various methods that reference them to reference the
new variables.
Updates the `Between` validator to no longer accept an array of options,
but instead three discrete arguments. It also sets the
`$messageVariables` property during instantiation.

Tests were refactored to use data providers, and to test for both valid
arguments and the expected behavior, as well as invalid constructor
arguments and expected exceptions.
@akrabat
Copy link
Contributor

akrabat commented Jul 18, 2017

The bigger issue is existing validators, and extensions to them. Developers extending existing validators may want to copy/paste implementations and begin migration of those to make them forwards-compatible with version 3. Since we would have version 3 released simultaneously to a v2 with the new interface additions, they could even copy those from v3 to aid their migration.

Every single custom validator and every extension to a supplied validator will need to be rewritten?

@weierophinney
Copy link
Member Author

Every single custom validator and every extension to a supplied validator will need to be rewritten?

Yes - as isValid() goes away, and is replaced by validate(), and the return values would differ. This is, of course, why it's targeted for a new major version.

My hope is that we can provide some tooling and guidance around this. For instance, something like the following trait would allow adapting an existing validator to implement the new Validator interface:

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

        return ValidatorResult::createInvalidResult(
            $value,
            $this->getMessageTemplates(),
            $this->abstractOptions['messageVariables']
        );
    }
}

class ExistingValidator extends AbstractValidator implements Validator
{
    use LegacyValidator;

    /* ... */
}

If we provide the legacy interface in v3, but mark it deprecated, this approach would allow mixing and matching existing v2-capable validators with v3, providing a longer upgrade path.

@akrabat
Copy link
Contributor

akrabat commented Jul 18, 2017

I think there's going to have to be a compelling advantage to v3 or upgrading is going to have to be quite easy :) A quick check of one of my projects shows that I have 30 or so Validators that would need upgrading and that won't be an easy sell to the client if there's no tangible benefit if it's more than a day's work.

I assume that the trait won't work for validators that extend current validators, so they'll have to be re-done.

@weierophinney
Copy link
Member Author

I think there's going to have to be a compelling advantage to v3

One of the issues currently is that stateful validators can lead to hard to debug issues, particularly if you share an instance.

As an example:

$validator->isValid($value1);
$validator->isValid($value2);
$messages = $validator->getMessages(); // $value1 messages are missing
$value = $validator->getValue(); // $value2

The other issue I've been seeing is maintenance of the AbstractValidator, as it contains a ton of logic around gathering options, formatting messages, etc:

  • Due to the options based approach, we have to do a lot of internal testing to see if the validator is in a valid state before we validate — does it have the necessary options? are they of the correct type? etc.
  • Moving presentation logic (value obscuration, translation, message truncation, etc.) into results, result decorators, or DTOs greatly reduces the logic, and thus maintenance, of that base class.

One nice benefit of the approach is that we can finally use callables for validation more cleanly. If they return a Result instance, this is no different than using a Validator for a consumer. The Callback validator can be used when you want to return a boolean result instead; you would compose your message templates and variables in that, so it can return a Result. This simplifies the creation and usage of one-off validators; you no longer need to have classes. (That said, PHP 7's anonymous classes also makes this easier.)

@weierophinney weierophinney mentioned this pull request Jul 18, 2017
/**
* Value object representing results of validation.
*/
class ValidatorResult implements Result

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make it final? We should promote good practics and teach developers to use patterns e.g. decorators

/**
* @param mixed $value
*/
public static function createValidResult($value) : self

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

/**
* @return iterable
*/
public function getIterator()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just getIterator(): iterable ?

*/
public function getIterator()
{
foreach ($this->results as $result) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is difference here to use just return $this->results; ?

public function __construct(
$value,
bool $isValid,
array $messageTemplates = [],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you think to make a collection value object for message templates and variables? We can keep types strictly.

@weierophinney
Copy link
Member Author

This repository has been closed and moved to laminas/laminas-validator; a new issue has been opened at laminas/laminas-validator#15.

@weierophinney
Copy link
Member Author

This repository has been moved to laminas/laminas-validator. If you feel that this patch is still relevant, please re-open against that repository, and reference this issue. To re-open, we suggest the following workflow:

  • Squash all commits in your branch (git rebase -i origin/{branch})
  • Make a note of all changed files (`git diff --name-only origin/{branch}...HEAD
  • Run the laminas/laminas-migration tool on the code.
  • Clone laminas/laminas-validator to another directory.
  • Copy the files from the second bullet point to the clone of laminas/laminas-validator.
  • In your clone of laminas/laminas-validator, commit the files, push to your fork, and open the new PR.
    We will be providing tooling via laminas/laminas-migration soon to help automate the process.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants