symfony/symfony — pull request #10287 by webmozart (2014-02-18 13:33:02+00:00)
Title: [WIP][Validator] New NodeTraverser implementation

This is a preview PR. I'd appreciate general, architectural feedback. Feedback about CS and the like is not useful yet, as I'm not done with the implementation yet.

#### Goal

The goal of this PR is to be able to fix the following tickets:

* #6138 Simplify adding of constraint violations
* #7146 Support group sequences in Validator API
* #7432 Poorly implemented Visitor Pattern
* #8617 Control traversal on class level
* #9888 Improve support for collection validation (PR: #9988)

Of course, full backwards compatibility should be guaranteed.

Other tickets I want to fix in follow-up PRs:

* #3622 Constraints\Valid does not respect "groups" option
* #4453 walk constraints by groups
* #7700 Propagate implicit group names in constraints
* #8376 Using validation_group causes error message to display multiple times
* #9051 Always ask value event if field isn't in the validating group
* #9819 Constraints applied multiple times when using interface inheritance
* #9939 GroupSequences still execute next group if first fail
* #10163 poor collection validation test coverage
* #10221 TypeValidator does not enforce desired type when value is NULL

#### In a nutshell

The implementation is a typical NodeTraverser/NodeVisitor implementation as seen in Twig or PHPParser (fixes #7432):

```php
interface NodeTraverserInterface
{
public function addVisitor(NodeVisitorInterface $visitor);
public function removeVisitor(NodeVisitorInterface $visitor);
public function traverse(array $nodes);
}

interface NodeVisitorInterface
{
public function beforeTraversal(array $nodes);
public function afterTraversal(array $nodes);
public function enterNode(Node $node);
public function leaveNode(Node $node);
}
```

Each object graph is modeled as a graph of `ClassNode` and `PropertyNode` instances, which is then traversed by the NodeTraverser. Each of the nodes is passed to all registered visitors, which implement the validation logic:

* GroupSequenceResolver turns the "Default" group into `GroupSequence` instances, if applicable
* ObjectInitializer runs the registered object initializers on each object
* ExecutionContextManager adapts the context information to the current node
* NodeValidator does the actual validation of a node

In the end, it should be possible to register custom visitors. This is not implemented yet.

#### Performance

Nodes are created during the traversal using a Depth-First approach, resulting in a memory usage of O(log n) [1], compared to O(n) if we create the node graph in advance.

Before, we created a new `ExecutionContext` instance per validated node. Now, we create a new `Node` instance per validated node, so the overall object count should be roughly the same.

I haven't done any concrete performance measurements yet. I expect that the performance of the old and the new implementation will be roughly the same, plus benefits caused by additional caches in the new implementation.

#### Features

##### Custom group sequences (#7146)

It is now possible to pass `GroupSequence` instances whenever you can pass a group to the validator. For example:

```php
$violations = $validator->validateObject($object, new GroupSequence('Basic', 'Strict'));
```

Or in the context of the Form component:

```php
$form = $this->createForm(new BlogType(), new Blog(), array(
'validation_groups' => new GroupSequence('Basic', 'Strict'),
));
```

##### Constraint violation builders (#6138)

The API for adding constraint violations was simplified:

```php
$this->context->addViolation('message', array('param' => 'value'));

// or

$this->context->buildViolation('message')
->atPath('property')
->setParameter('param', 'value')
->setTranslationDomain('validation_strict')
->addViolation();
```

##### Control the context of nested validations

Currently, you can validate objects or constraints from within a constraint validator like this:

```php
$this->context->validate($object);
$this->context->validateValue($value, new Length(array('min' => 3)));
```

The validations will run in and add violations to the current validation context.

This way, it is impossible though to validate something, inspect the result and then decide what kind of violations to add to the context. This is needed, for example, for the `Some` constraint (proposed in #9888), which should succeed if any of the validated values did *not* generate violations.

For this reason, the new context API features a method `getValidator()`. This method returns the validator instance, you can use it to validate anything in a new context (as the validator always does):

```php
$validator = $this->context->getValidator();
$violations = $validator->validate($object);

if (count($violations) > 0) {
$this->context->addViolation('The validation did not pass');
}
```

If you want to execute the validation in the current context, use the `inContext()` method of the validator. You can also use `atPath()` to adjust the property path:

```php
// violations are added to $this->context
$validator
->inContext($this->context)
->atPath('lastName')
->validateValue($value, new Length(array('min' => 3)))
;
```

With this feature, #9888 (especially the PR for it: #9988) can be finished.

##### Control traversal at class level (#8617)

Currently, it is possible whether to traverse a `Traversable` object or not in the `Valid` constraint:

```php
/**
* @Assert\Valid(traverse=true)
*/
private $tags = new TagList();
```

(actually, `true` is the default)

In this way, the validator will iterate the `TagList` instance and validate each of the contained objects. You can also set "traverse" to `false` to disable iteration.

What if you want to specify, that `TagList` instances should always (or never) be traversed? That's currently not possible.

With this PR, you will be able to do the following (the `Traverse` constraint is not implemented yet, but the basic functionality is):

```php
/**
* @Assert\Traverse(false)
*/
class TagList implements \IteratorAggregate
{
// ...
}
```

#### Follow-up features

Features of the follow-up PRs will be described directly there.

#### Backwards compatibility

I implemented a new `AbstractValidatorTest` which tests both the old and the new implementation for compatibility. I still want to extend this test to make sure we don't introduce any regressions.

Almost none of the existing classes were modified (or only slightly). If users depend on the current (now "legacy") implementation, they will have the choice to continue using it until 3.0 (of course, without the new features).

#### Your task

Congrats, you made it till here :) If you have time, please skim over the code and give me feedback on the overall implementation. Again, no feedback on details yet, there are quite a few areas in the code that are still work in progress.

Thanks,
Bernhard

[1] That means that only the nodes from the root of the graph until the currently validated node are held in memory.