From efe42cbb1f284b992d8de9f136c3b20848bee7f9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 22 Nov 2012 15:58:46 +0100 Subject: [PATCH] [Validator] Refactored the GraphWalker into an implementation of the Visitor design pattern. With this refactoring comes a decoupling of the validator from the structure of the underlying metadata. This way it is possible for Drupal to use the validator for validating their Entity API by using their own metadata layer, which is not modeled as classes and properties/getter methods. --- src/Symfony/Component/Validator/CHANGELOG.md | 21 + .../Validator/ClassBasedInterface.php | 27 + .../Validator/ConstraintValidator.php | 4 +- .../ConstraintValidatorInterface.php | 4 +- .../Validator/ConstraintViolation.php | 96 +++- .../ConstraintViolationInterface.php | 136 +++++ .../Validator/ConstraintViolationList.php | 85 +-- .../ConstraintViolationListInterface.php | 83 +++ .../Exception/NoSuchMetadataException.php | 19 + .../Component/Validator/ExecutionContext.php | 280 ++++++++-- .../Validator/ExecutionContextInterface.php | 304 +++++++++++ .../Validator/GlobalExecutionContext.php | 76 --- .../GlobalExecutionContextInterface.php | 68 +++ .../Component/Validator/GraphWalker.php | 207 +++---- .../Validator/Mapping/ClassMetadata.php | 50 +- .../Mapping/ClassMetadataFactory.php | 50 +- .../Mapping/ClassMetadataFactoryAdapter.php | 59 ++ .../Mapping/ClassMetadataFactoryInterface.php | 13 + .../Validator/Mapping/GetterMetadata.php | 2 +- .../Validator/Mapping/MemberMetadata.php | 22 +- .../Validator/Mapping/PropertyMetadata.php | 2 +- .../Validator/MetadataFactoryInterface.php | 40 ++ .../Component/Validator/MetadataInterface.php | 68 +++ .../Validator/ObjectInitializerInterface.php | 7 +- .../PropertyMetadataContainerInterface.php | 33 ++ .../Validator/PropertyMetadataInterface.php | 44 ++ .../Validator/Tests/ExecutionContextTest.php | 103 +++- .../Tests/Fixtures/ConstraintAValidator.php | 4 +- .../Fixtures/FakeClassMetadataFactory.php | 34 -- .../Tests/Fixtures/FakeMetadataFactory.php | 56 ++ .../Tests/GlobalExecutionContextTest.php | 63 --- .../Validator/Tests/GraphWalkerTest.php | 67 ++- .../Tests/Mapping/MemberMetadataTest.php | 2 +- .../Validator/Tests/ValidationVisitorTest.php | 508 ++++++++++++++++++ .../Validator/Tests/ValidatorContextTest.php | 3 +- .../Validator/Tests/ValidatorFactoryTest.php | 3 +- .../Validator/Tests/ValidatorTest.php | 71 ++- .../Component/Validator/ValidationVisitor.php | 204 +++++++ .../Validator/ValidationVisitorInterface.php | 90 ++++ src/Symfony/Component/Validator/Validator.php | 214 +++++--- .../Component/Validator/ValidatorBuilder.php | 10 +- .../Validator/ValidatorBuilderInterface.php | 8 +- .../Component/Validator/ValidatorContext.php | 14 +- .../Validator/ValidatorInterface.php | 70 ++- 44 files changed, 2733 insertions(+), 591 deletions(-) create mode 100644 src/Symfony/Component/Validator/ClassBasedInterface.php create mode 100644 src/Symfony/Component/Validator/ConstraintViolationInterface.php create mode 100644 src/Symfony/Component/Validator/ConstraintViolationListInterface.php create mode 100644 src/Symfony/Component/Validator/Exception/NoSuchMetadataException.php create mode 100644 src/Symfony/Component/Validator/ExecutionContextInterface.php delete mode 100644 src/Symfony/Component/Validator/GlobalExecutionContext.php create mode 100644 src/Symfony/Component/Validator/GlobalExecutionContextInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryAdapter.php create mode 100644 src/Symfony/Component/Validator/MetadataFactoryInterface.php create mode 100644 src/Symfony/Component/Validator/MetadataInterface.php create mode 100644 src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php create mode 100644 src/Symfony/Component/Validator/PropertyMetadataInterface.php delete mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadataFactory.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php delete mode 100644 src/Symfony/Component/Validator/Tests/GlobalExecutionContextTest.php create mode 100644 src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php create mode 100644 src/Symfony/Component/Validator/ValidationVisitor.php create mode 100644 src/Symfony/Component/Validator/ValidationVisitorInterface.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 5a385d3a93..41f311cac5 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -6,6 +6,27 @@ CHANGELOG * added a CardScheme validator * added a Luhn validator + * moved @api-tags from `Validator` to `ValidatorInterface` + * moved @api-tags from `ConstraintViolation` to the new `ConstraintViolationInterface` + * moved @api-tags from `ConstraintViolationList` to the new `ConstraintViolationListInterface` + * moved @api-tags from `ExecutionContext` to the new `ExecutionContextInterface` + * [BC BREAK] `ConstraintValidatorInterface::initialize` is now type hinted against `ExecutionContextInterface` instead of `ExecutionContext` + * [BC BREAK] changed the visibility of the properties in `Validator` from protected to private + * deprecated `ClassMetadataFactoryInterface` in favor of the new `MetadataFactoryInterface` + * deprecated `ClassMetadataFactory::getClassMetadata` in favor of `getMetadataFor` + * created `MetadataInterface`, `PropertyMetadataInterface`, `ClassBasedInterface` and `PropertyMetadataContainerInterface` + * deprecated `GraphWalker` in favor of the new `ValidationVisitorInterface` + * deprecated `ExecutionContext::addViolationAtPath` + * deprecated `ExecutionContext::addViolationAtSubPath` in favor of `ExecutionContextInterface::addViolationAt` + * deprecated `ExecutionContext::getCurrentClass` in favor of `ExecutionContextInterface::getClassName` + * deprecated `ExecutionContext::getCurrentProperty` in favor of `ExecutionContextInterface::getPropertyName` + * deprecated `ExecutionContext::getCurrentValue` in favor of `ExecutionContextInterface::getValue` + * deprecated `ExecutionContext::getGraphWalker` in favor of `ExecutionContextInterface::validate` and `ExecutionContextInterface::validateValue` + * deprecated `ExecutionContext::getMetadataFactory` in favor of `ExecutionContextInterface::getMetadataFor` + * improved `ValidatorInterface::validateValue` to accept arrays of constraints + * changed `ValidatorInterface::getMetadataFactory` to return a `MetadataFactoryInterface` instead of a `ClassMetadataFactoryInterface` + * removed `ClassMetadataFactoryInterface` type hint from `ValidatorBuilderInterface::setMetadataFactory`. + As of Symfony 2.3, this method will be typed against `MetadataFactoryInterface` instead. 2.1.0 ----- diff --git a/src/Symfony/Component/Validator/ClassBasedInterface.php b/src/Symfony/Component/Validator/ClassBasedInterface.php new file mode 100644 index 0000000000..c8fa25d43d --- /dev/null +++ b/src/Symfony/Component/Validator/ClassBasedInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * An object backed by a PHP class. + * + * @author Bernhard Schussek + */ +interface ClassBasedInterface +{ + /** + * Returns the name of the backing PHP class. + * + * @return string The name of the backing class. + */ + public function getClassName(); +} diff --git a/src/Symfony/Component/Validator/ConstraintValidator.php b/src/Symfony/Component/Validator/ConstraintValidator.php index 7ad058fa38..cf58c70f25 100644 --- a/src/Symfony/Component/Validator/ConstraintValidator.php +++ b/src/Symfony/Component/Validator/ConstraintValidator.php @@ -23,7 +23,7 @@ use Symfony\Component\Validator\Exception\ValidatorException; abstract class ConstraintValidator implements ConstraintValidatorInterface { /** - * @var ExecutionContext + * @var ExecutionContextInterface */ protected $context; @@ -44,7 +44,7 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface /** * {@inheritDoc} */ - public function initialize(ExecutionContext $context) + public function initialize(ExecutionContextInterface $context) { $this->context = $context; $this->messageTemplate = ''; diff --git a/src/Symfony/Component/Validator/ConstraintValidatorInterface.php b/src/Symfony/Component/Validator/ConstraintValidatorInterface.php index c66bea72f4..f7538a1fef 100644 --- a/src/Symfony/Component/Validator/ConstraintValidatorInterface.php +++ b/src/Symfony/Component/Validator/ConstraintValidatorInterface.php @@ -21,9 +21,9 @@ interface ConstraintValidatorInterface /** * Initializes the constraint validator. * - * @param ExecutionContext $context The current validation context + * @param ExecutionContextInterface $context The current validation context */ - public function initialize(ExecutionContext $context); + public function initialize(ExecutionContextInterface $context); /** * Checks if the passed value is valid. diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 101a5d72b2..7b6f1e1928 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -12,20 +12,64 @@ namespace Symfony\Component\Validator; /** - * Represents a single violation of a constraint. + * Default implementation of {@ConstraintViolationInterface}. * - * @api + * @author Bernhard Schussek */ -class ConstraintViolation +class ConstraintViolation implements ConstraintViolationInterface { - protected $messageTemplate; - protected $messageParameters; - protected $messagePluralization; - protected $root; - protected $propertyPath; - protected $invalidValue; - protected $code; + /** + * @var string + */ + private $messageTemplate; + /** + * @var array + */ + private $messageParameters; + + /** + * @var integer|null + */ + private $messagePluralization; + + /** + * @var mixed + */ + private $root; + + /** + * @var string + */ + private $propertyPath; + + /** + * @var mixed + */ + private $invalidValue; + + /** + * @var mixed + */ + private $code; + + /** + * Creates a new constraint violation. + * + * @param string $messageTemplate The raw violation message. + * @param array $messageParameters The parameters to substitute + * in the raw message. + * @param mixed $root The value originally passed + * to the validator. + * @param string $propertyPath The property path from the + * root value to the invalid + * value. + * @param mixed $invalidValue The invalid value causing the + * violation. + * @param integer|null $messagePluralization The pluralization parameter. + * @param mixed $code The error code of the + * violation, if any. + */ public function __construct($messageTemplate, array $messageParameters, $root, $propertyPath, $invalidValue, $messagePluralization = null, $code = null) { $this->messageTemplate = $messageTemplate; @@ -38,7 +82,9 @@ class ConstraintViolation } /** - * @return string + * Converts the violation into a string for debugging purposes. + * + * @return string The violation as string. */ public function __toString() { @@ -58,9 +104,7 @@ class ConstraintViolation } /** - * @return string - * - * @api + * {@inheritDoc} */ public function getMessageTemplate() { @@ -68,9 +112,7 @@ class ConstraintViolation } /** - * @return array - * - * @api + * {@inheritDoc} */ public function getMessageParameters() { @@ -78,7 +120,7 @@ class ConstraintViolation } /** - * @return integer|null + * {@inheritDoc} */ public function getMessagePluralization() { @@ -86,11 +128,7 @@ class ConstraintViolation } /** - * Returns the violation message. - * - * @return string - * - * @api + * {@inheritDoc} */ public function getMessage() { @@ -105,21 +143,33 @@ class ConstraintViolation return strtr($this->messageTemplate, $parameters); } + /** + * {@inheritDoc} + */ public function getRoot() { return $this->root; } + /** + * {@inheritDoc} + */ public function getPropertyPath() { return $this->propertyPath; } + /** + * {@inheritDoc} + */ public function getInvalidValue() { return $this->invalidValue; } + /** + * {@inheritDoc} + */ public function getCode() { return $this->code; diff --git a/src/Symfony/Component/Validator/ConstraintViolationInterface.php b/src/Symfony/Component/Validator/ConstraintViolationInterface.php new file mode 100644 index 0000000000..cf1d5b9967 --- /dev/null +++ b/src/Symfony/Component/Validator/ConstraintViolationInterface.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * A violation of a constraint that happened during validation. + * + * For each constraint that fails during validation one or more violations are + * created. The violations store the violation message, the path to the failing + * element in the validation graph and the root element that was originally + * passed to the validator. For example, take the following graph: + * + *
+ * (Person)---(firstName: string)
+ *      \
+ *   (address: Address)---(street: string)
+ * 
+ * + * If the Person object is validated and validation fails for the + * "firstName" property, the generated violation has the Person + * instance as root and the property path "firstName". If validation fails + * for the "street" property of the related Address instance, the root + * element is still the person, but the property path is "address.street". + * + * @author Bernhard Schussek + * + * @api + */ +interface ConstraintViolationInterface +{ + /** + * Returns the violation message. + * + * @return string The violation message. + * + * @api + */ + public function getMessage(); + + /** + * Returns the raw violation message. + * + * The raw violation message contains placeholders for the parameters + * returned by {@link getMessageParameters}. Typically you'll pass the + * message template and parameters to a translation engine. + * + * @return string The raw violation message. + * + * @api + */ + public function getMessageTemplate(); + + /** + * Returns the parameters to be inserted into the raw violation message. + * + * @return array A possibly empty list of parameters indexed by the names + * that appear in the message template. + * + * @see getMessageTemplate + * + * @api + */ + public function getMessageParameters(); + + /** + * Returns a number for pluralizing the violation message. + * + * For example, the message template could have different translation based + * on a parameter "choices": + * + *
    + *
  • Please select exactly one entry. (choices=1)
  • + *
  • Please select two entries. (choices=2)
  • + *
+ * + * This method returns the value of the parameter for choosing the right + * pluralization form (in this case "choices"). + * + * @return integer|null The number to use to pluralize of the message. + */ + public function getMessagePluralization(); + + /** + * Returns the root element of the validation. + * + * @return mixed The value that was passed originally to the validator when + * the validation was started. Because the validator traverses + * the object graph, the value at which the violation occurs + * is not necessarily the value that was originally validated. + * + * @api + */ + public function getRoot(); + + /** + * Returns the property path from the root element to the violation. + * + * @return string The property path indicates how the validator reached + * the invalid value from the root element. If the root + * element is a Person instance with a property + * "address" that contains an Address instance + * with an invalid property "street", the generated property + * path is "address.street". Property access is denoted by + * dots, while array access is denoted by square brackets, + * for example "addresses[1].street". + * + * @api + */ + public function getPropertyPath(); + + /** + * Returns the value that caused the violation. + * + * @return mixed The invalid value that caused the validated constraint to + * fail. + * + * @api + */ + public function getInvalidValue(); + + /** + * Returns a machine-digestible error code for the violation. + * + * @return mixed The error code. + */ + public function getCode(); +} diff --git a/src/Symfony/Component/Validator/ConstraintViolationList.php b/src/Symfony/Component/Validator/ConstraintViolationList.php index 0f19ffa4d8..ef5a196d02 100644 --- a/src/Symfony/Component/Validator/ConstraintViolationList.php +++ b/src/Symfony/Component/Validator/ConstraintViolationList.php @@ -12,25 +12,21 @@ namespace Symfony\Component\Validator; /** - * A list of ConstrainViolation objects. + * Default implementation of {@ConstraintViolationListInterface}. * * @author Bernhard Schussek - * - * @api */ -class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayAccess +class ConstraintViolationList implements \IteratorAggregate, ConstraintViolationListInterface { /** - * The constraint violations - * - * @var array + * @var ConstraintViolationInterface[] */ - protected $violations = array(); + private $violations = array(); /** * Creates a new constraint violation list. * - * @param array $violations The constraint violations to add to the list + * @param ConstraintViolationInterface[] $violations The constraint violations to add to the list */ public function __construct(array $violations = array()) { @@ -40,7 +36,9 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @return string + * Converts the violation into a string for debugging purposes. + * + * @return string The violation as string. */ public function __toString() { @@ -54,39 +52,25 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * Add a ConstraintViolation to this list. - * - * @param ConstraintViolation $violation - * - * @api + * {@inheritDoc} */ - public function add(ConstraintViolation $violation) + public function add(ConstraintViolationInterface $violation) { $this->violations[] = $violation; } /** - * Merge an existing ConstraintViolationList into this list. - * - * @param ConstraintViolationList $otherList - * - * @api + * {@inheritDoc} */ - public function addAll(ConstraintViolationList $otherList) + public function addAll(ConstraintViolationListInterface $otherList) { - foreach ($otherList->violations as $violation) { + foreach ($otherList as $violation) { $this->violations[] = $violation; } } /** - * Returns the violation at a given offset. - * - * @param integer $offset The offset of the violation. - * - * @return ConstraintViolation The violation. - * - * @throws \OutOfBoundsException If the offset does not exist. + * {@inheritDoc} */ public function get($offset) { @@ -98,11 +82,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * Returns whether the given offset exists. - * - * @param integer $offset The violation offset. - * - * @return Boolean Whether the offset exists. + * {@inheritDoc} */ public function has($offset) { @@ -110,20 +90,15 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * Sets a violation at a given offset. - * - * @param integer $offset The violation offset. - * @param ConstraintViolation $violation The violation. + * {@inheritDoc} */ - public function set($offset, ConstraintViolation $violation) + public function set($offset, ConstraintViolationInterface $violation) { $this->violations[$offset] = $violation; } /** - * Removes a violation at a given offset. - * - * @param integer $offset The offset to remove. + * {@inheritDoc} */ public function remove($offset) { @@ -131,9 +106,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @see IteratorAggregate - * - * @api + * {@inheritDoc} */ public function getIterator() { @@ -141,9 +114,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @see Countable - * - * @api + * {@inheritDoc} */ public function count() { @@ -151,9 +122,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @see ArrayAccess - * - * @api + * {@inheritDoc} */ public function offsetExists($offset) { @@ -161,9 +130,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @see ArrayAccess - * - * @api + * {@inheritDoc} */ public function offsetGet($offset) { @@ -171,9 +138,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @see ArrayAccess - * - * @api + * {@inheritDoc} */ public function offsetSet($offset, $violation) { @@ -185,9 +150,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA } /** - * @see ArrayAccess - * - * @api + * {@inheritDoc} */ public function offsetUnset($offset) { diff --git a/src/Symfony/Component/Validator/ConstraintViolationListInterface.php b/src/Symfony/Component/Validator/ConstraintViolationListInterface.php new file mode 100644 index 0000000000..21b857ec09 --- /dev/null +++ b/src/Symfony/Component/Validator/ConstraintViolationListInterface.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * A list of constraint violations. + * + * @author Bernhard Schussek + * + * @api + */ +interface ConstraintViolationListInterface extends \Traversable, \Countable, \ArrayAccess +{ + /** + * Adds a constraint violation to this list. + * + * @param ConstraintViolationInterface $violation The violation to add. + * + * @api + */ + public function add(ConstraintViolationInterface $violation); + + /** + * Merges an existing violation list into this list. + * + * @param ConstraintViolationListInterface $otherList The list to merge. + * + * @api + */ + public function addAll(ConstraintViolationListInterface $otherList); + + /** + * Returns the violation at a given offset. + * + * @param integer $offset The offset of the violation. + * + * @return ConstraintViolationInterface The violation. + * + * @throws \OutOfBoundsException If the offset does not exist. + * + * @api + */ + public function get($offset); + + /** + * Returns whether the given offset exists. + * + * @param integer $offset The violation offset. + * + * @return Boolean Whether the offset exists. + * + * @api + */ + public function has($offset); + + /** + * Sets a violation at a given offset. + * + * @param integer $offset The violation offset. + * @param ConstraintViolationInterface $violation The violation. + * + * @api + */ + public function set($offset, ConstraintViolationInterface $violation); + + /** + * Removes a violation at a given offset. + * + * @param integer $offset The offset to remove. + * + * @api + */ + public function remove($offset); +} diff --git a/src/Symfony/Component/Validator/Exception/NoSuchMetadataException.php b/src/Symfony/Component/Validator/Exception/NoSuchMetadataException.php new file mode 100644 index 0000000000..4cac74cf30 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/NoSuchMetadataException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Exception; + +/** + * @author Bernhard Schussek + */ +class NoSuchMetadataException extends ValidatorException +{ +} diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index eddc9dfc28..af9b2e6b32 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -11,59 +11,69 @@ namespace Symfony\Component\Validator; -use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; - /** - * Stores the state of the current node in the validation graph. + * Default implementation of {@link ExecutionContextInterface}. * * This class is immutable by design. * - * It is used by the GraphWalker to initialize validation of different items - * and keep track of the violations. - * * @author Fabien Potencier * @author Bernhard Schussek - * - * @api */ -class ExecutionContext +class ExecutionContext implements ExecutionContextInterface { + /** + * @var GlobalExecutionContextInterface + */ private $globalContext; - private $propertyPath; - private $value; - private $group; - private $class; - private $property; - public function __construct(GlobalExecutionContext $globalContext, $value, $propertyPath, $group, $class = null, $property = null) + /** + * @var MetadataInterface + */ + private $metadata; + + /** + * @var mixed + */ + private $value; + + /** + * @var string + */ + private $group; + + /** + * @var string + */ + private $propertyPath; + + /** + * Creates a new execution context. + * + * @param GlobalExecutionContextInterface $globalContext The global context storing node-independent state. + * @param MetadataInterface $metadata The metadata of the validated node. + * @param mixed $value The value of the validated node. + * @param string $group The current validation group. + * @param string $propertyPath The property path to the current node. + */ + public function __construct(GlobalExecutionContextInterface $globalContext, MetadataInterface $metadata = null, $value = null, $group = null, $propertyPath = '') { + if (null === $group) { + $group = Constraint::DEFAULT_GROUP; + } + $this->globalContext = $globalContext; + $this->metadata = $metadata; $this->value = $value; $this->propertyPath = $propertyPath; $this->group = $group; - $this->class = $class; - $this->property = $property; - } - - public function __clone() - { - $this->globalContext = clone $this->globalContext; } /** - * Adds a violation at the current node of the validation graph. - * - * @param string $message The error message. - * @param array $params The parameters parsed into the error message. - * @param mixed $invalidValue The invalid, validated value. - * @param integer|null $pluralization The number to use to pluralize of the message. - * @param integer|null $code The violation code. - * - * @api + * {@inheritdoc} */ public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) { - $this->globalContext->addViolation(new ConstraintViolation( + $this->globalContext->getViolations()->add(new ConstraintViolation( $message, $params, $this->globalContext->getRoot(), @@ -85,10 +95,12 @@ class ExecutionContext * @param mixed $invalidValue The invalid, validated value. * @param integer|null $pluralization The number to use to pluralize of the message. * @param integer|null $code The violation code. + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. */ public function addViolationAtPath($propertyPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) { - $this->globalContext->addViolation(new ConstraintViolation( + $this->globalContext->getViolations()->add(new ConstraintViolation( $message, $params, $this->globalContext->getRoot(), @@ -110,10 +122,26 @@ class ExecutionContext * @param mixed $invalidValue The invalid, validated value. * @param integer|null $pluralization The number to use to pluralize of the message. * @param integer|null $code The violation code. + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use the + * method {@link atViolationAt} instead. */ public function addViolationAtSubPath($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) { - $this->globalContext->addViolation(new ConstraintViolation( + if (func_num_args() >= 4) { + $this->addViolationAt($subPath, $message, $params, $invalidValue, $pluralization, $code); + } else { + // Needed in order to make the check for func_num_args() inside work + $this->addViolationAt($subPath, $message, $params); + } + } + + /** + * {@inheritdoc} + */ + public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + { + $this->globalContext->getViolations()->add(new ConstraintViolation( $message, $params, $this->globalContext->getRoot(), @@ -126,20 +154,24 @@ class ExecutionContext } /** - * @return ConstraintViolationList - * - * @api + * {@inheritdoc} */ public function getViolations() { return $this->globalContext->getViolations(); } + /** + * {@inheritdoc} + */ public function getRoot() { return $this->globalContext->getRoot(); } + /** + * {@inheritdoc} + */ public function getPropertyPath($subPath = null) { if (null !== $subPath && '' !== $this->propertyPath && '' !== $subPath && '[' !== $subPath[0]) { @@ -149,39 +181,195 @@ class ExecutionContext return $this->propertyPath . $subPath; } - public function getCurrentClass() + /** + * {@inheritdoc} + */ + public function getClassName() { - return $this->class; + if ($this->metadata instanceof ClassBasedInterface) { + return $this->metadata->getClassName(); + } + + return null; } - public function getCurrentProperty() + /** + * {@inheritdoc} + */ + public function getPropertyName() { - return $this->property; + if ($this->metadata instanceof PropertyMetadataInterface) { + return $this->metadata->getPropertyName(); + } + + return null; } - public function getCurrentValue() + /** + * {@inheritdoc} + */ + public function getValue() { return $this->value; } + /** + * {@inheritdoc} + */ public function getGroup() { return $this->group; } /** - * @return GraphWalker + * {@inheritdoc} */ - public function getGraphWalker() + public function getMetadata() { - return $this->globalContext->getGraphWalker(); + return $this->metadata; } /** - * @return ClassMetadataFactoryInterface + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + return $this->globalContext->getMetadataFactory()->getMetadataFor($value); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $groups = null, $subPath = '', $traverse = false, $deep = false) + { + $propertyPath = $this->getPropertyPath($subPath); + + foreach ($this->resolveGroups($groups) as $group) { + $this->globalContext->getVisitor()->validate($value, $group, $propertyPath, $traverse, $deep); + } + } + + /** + * {@inheritdoc} + */ + public function validateValue($value, $constraints, $groups = null, $subPath = '') + { + $constraints = is_array($constraints) ? $constraints : array($constraints); + + if (null === $groups && '' === $subPath) { + $context = clone $this; + $context->value = $value; + $context->executeConstraintValidators($value, $constraints); + + return; + } + + $propertyPath = $this->getPropertyPath($subPath); + + foreach ($this->resolveGroups($groups) as $group) { + $context = clone $this; + $context->value = $value; + $context->group = $group; + $context->propertyPath = $propertyPath; + $context->executeConstraintValidators($value, $constraints); + } + } + + /** + * Returns the class name of the current node. + * + * @return string|null The class name or null, if the current node does not + * hold information about a class. + * + * @see getClassName + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use + * {@link getClassName} instead. + */ + public function getCurrentClass() + { + return $this->getClassName(); + } + + /** + * Returns the property name of the current node. + * + * @return string|null The property name or null, if the current node does + * not hold information about a property. + * + * @see getPropertyName + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use + * {@link getClassName} instead. + */ + public function getCurrentProperty() + { + return $this->getPropertyName(); + } + + /** + * Returns the currently validated value. + * + * @return mixed The current value. + * + * @see getValue + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use + * {@link getValue} instead. + */ + public function getCurrentValue() + { + return $this->value; + } + + /** + * Returns the graph walker instance. + * + * @return GraphWalker The graph walker. + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use + * {@link validate} and {@link validateValue} instead. + */ + public function getGraphWalker() + { + return $this->globalContext->getVisitor()->getGraphWalker(); + } + + /** + * {@inheritdoc} */ public function getMetadataFactory() { return $this->globalContext->getMetadataFactory(); } + + /** + * Executes the validators of the given constraints for the given value. + * + * @param mixed $value The value to validate. + * @param Constraint[] $constraints The constraints to match against. + */ + private function executeConstraintValidators($value, array $constraints) + { + foreach ($constraints as $constraint) { + $validator = $this->globalContext->getValidatorFactory()->getInstance($constraint); + $validator->initialize($this); + $validator->validate($value, $constraint); + } + } + + /** + * Returns an array of group names. + * + * @param null|string|string[] $groups The groups to resolve. If a single string is + * passed, it is converted to an array. If null + * is passed, an array containing the current + * group of the context is returned. + * + * @return array An array of validation groups. + */ + private function resolveGroups($groups) + { + return $groups ? (array) $groups : (array) $this->group; + } } diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php new file mode 100644 index 0000000000..cae91a99b4 --- /dev/null +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -0,0 +1,304 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * Stores the validator's state during validation. + * + * For example, let's validate the following object graph: + * + *
+ * (Person)---($firstName: string)
+ *      \
+ *   ($address: Address)---($street: string)
+ * 
+ * + * We validate the Person instance, which becomes the "root" of the + * validation run (see {@link getRoot}). The state of the context after the + * first step will be like this: + * + *
+ * (Person)---($firstName: string)
+ *    ^ \
+ *   ($address: Address)---($street: string)
+ * 
+ * + * The validator is stopped at the Person node, both the root and the + * value (see {@link getValue}) of the context point to the Person + * instance. The property path is empty at this point (see {@link getPropertyPath}). + * The metadata of the context is the metadata of the Person node + * (see {@link getMetadata}). + * + * After advancing to the property $firstName of the Person + * instance, the state of the context looks like this: + * + *
+ * (Person)---($firstName: string)
+ *      \              ^
+ *   ($address: Address)---($street: string)
+ * 
+ * + * The validator is stopped at the property $firstName. The root still + * points to the Person instance, because this is where the validation + * started. The property path is now "firstName" and the current value is the + * value of that property. + * + * After advancing to the $address property and then to the + * $street property of the Address instance, the context state + * looks like this: + * + *
+ * (Person)---($firstName: string)
+ *      \
+ *   ($address: Address)---($street: string)
+ *                               ^
+ * 
+ * + * The validator is stopped at the property $street. The root still + * points to the Person instance, but the property path is now + * "address.street" and the validated value is the value of that property. + * + * Apart from the root, the property path and the currently validated value, + * the execution context also knows the metadata of the current node (see + * {@link getMetadata}) which for example returns a {@link Mapping\PropertyMetadata} + * or a {@link Mapping\ClassMetadata} object. he context also contains the + * validation group that is currently being validated (see {@link getGroup}) and + * the violations that happened up until now (see {@link getViolations}). + * + * Apart from reading the execution context, you can also use + * {@link addViolation} or {@link addViolationAt} to add new violations and + * {@link validate} or {@link validateValue} to validate values that the + * validator otherwise would not reach. + * + * @author Bernhard Schussek + * + * @api + */ +interface ExecutionContextInterface +{ + /** + * Adds a violation at the current node of the validation graph. + * + * @param string $message The error message. + * @param array $params The parameters substituted in the error message. + * @param mixed $invalidValue The invalid, validated value. + * @param integer|null $pluralization The number to use to pluralize of the message. + * @param integer|null $code The violation code. + * + * @api + */ + public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); + + /** + * Adds a violation at the validation graph node with the given property + * path relative to the current property path. + * + * @param string $subPath The relative property path for the violation. + * @param string $message The error message. + * @param array $params The parameters substituted in the error message. + * @param mixed $invalidValue The invalid, validated value. + * @param integer|null $pluralization The number to use to pluralize of the message. + * @param integer|null $code The violation code. + * + * @api + */ + public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); + + /** + * Validates the given value within the scope of the current validation. + * + * The value may be any value recognized by the used metadata factory + * (see {@link MetadataFactoryInterface::getMetadata}), or an array or a + * traversable object of such values. + * + * Usually you validate a value that is not the current node of the + * execution context. For this case, you can pass the {@link $subPath} + * argument which is appended to the current property path when a violation + * is created. For example, take the following object graph: + * + *
+     * (Person)---($address: Address)---($phoneNumber: PhoneNumber)
+     *                     ^
+     * 
+ * + * When the execution context stops at the Person instance, the + * property path is "address". When you validate the PhoneNumber + * instance now, pass "phoneNumber" as sub path to correct the property path + * to "address.phoneNumber": + * + *
+     * $context->validate($address->phoneNumber, 'phoneNumber');
+     * 
+ * + * Any violations generated during the validation will be added to the + * violation list that you can access with {@link getViolations}. + * + * @param mixed $value The value to validate. + * @param string $subPath The path to append to the context's property path. + * @param null $groups The groups to validate in. If you don't pass any + * groups here, the current group of the context + * will be used. + * @param bool $traverse Whether to traverse the value if it is an array + * or an instance of \Traversable. + * @param bool $deep Whether to traverse the value recursively if + * it is a collection of collections. + */ + public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false); + + /** + * Validates a value against a constraint. + * + * Use the parameter $subPath to adapt the property path for the + * validated value. For example, take the following object graph: + * + *
+     * (Person)---($address: Address)---($street: string)
+     *                     ^
+     * 
+ * + * When the validator validates the Address instance, the + * property path stored in the execution context is "address". When you + * manually validate the property $street now, pass the sub path + * "street" to adapt the full property path to "address.street": + * + *
+     * $context->validate($address->street, new NotNull(), 'street');
+     * 
+ * + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param string $subPath The path to append to the context's property path. + * @param null $groups The groups to validate in. If you don't pass any + * groups here, the current group of the context + * will be used. + */ + public function validateValue($value, $constraints, $subPath = '', $groups = null); + + /** + * Returns the violations generated by the validator so far. + * + * @return ConstraintViolationListInterface The constraint violation list. + * + * @api + */ + public function getViolations(); + + /** + * Returns the value at which validation was started in the object graph. + * + * The validator, when given an object, traverses the properties and + * related objects and their properties. The root of the validation is the + * object from which the traversal started. + * + * The current value is returned by {@link getValue}. + * + * @return mixed The root value of the validation. + */ + public function getRoot(); + + /** + * Returns the value that the validator is currently validating. + * + * If you want to retrieve the object that was originally passed to the + * validator, use {@link getRoot}. + * + * @return mixed The currently validated value. + */ + public function getValue(); + + /** + * Returns the metadata for the currently validated value. + * + * With the core implementation, this method returns a + * {@link Mapping\ClassMetadata} instance if the current value is an object, + * a {@link Mapping\PropertyMetadata} instance if the current value is + * the value of a property and a {@link Mapping\GetterMetadata} instance if + * the validated value is the result of a getter method. + * + * If the validated value is neither of these, for example if the validator + * has been called with a plain value and constraint, this method returns + * null. + * + * @return MetadataInterface|null The metadata of the currently validated + * value. + */ + public function getMetadata(); + + /** + * Returns the used metadata factory. + * + * @return MetadataFactoryInterface The metadata factory. + */ + public function getMetadataFactory(); + + /** + * Returns the validation group that is currently being validated. + * + * @return string The current validation group. + */ + public function getGroup(); + + /** + * Returns the class name of the current node. + * + * If the metadata of the current node does not implement + * {@link ClassBasedInterface} or if no metadata is available for the + * current node, this method returns null. + * + * @return string|null The class name or null, if no class name could be found. + */ + public function getClassName(); + + /** + * Returns the property name of the current node. + * + * If the metadata of the current node does not implement + * {@link PropertyMetadataInterface} or if no metadata is available for the + * current node, this method returns null. + * + * @return string|null The property name or null, if no property name could be found. + */ + public function getPropertyName(); + + /** + * Returns the property path to the value that the validator is currently + * validating. + * + * For example, take the following object graph: + * + *
+     * (Person)---($address: Address)---($street: string)
+     * 
+ * + * When the Person instance is passed to the validator, the + * property path is initially empty. When the $address property + * of that person is validated, the property path is "address". When + * the $street property of the related Address instance + * is validated, the property path is "address.street". + * + * Properties of objects are prefixed with a dot in the property path. + * Indices of arrays or objects implementing the {@link \ArrayAccess} + * interface are enclosed in brackets. For example, if the property in + * the previous example is $addresses and contains an array + * of Address instance, the property path generated for the + * $street property of one of these addresses is for example + * "addresses[0].street". + * + * @param string $subPath Optional. The suffix appended to the current + * property path. + * + * @return string The current property path. The result may be an empty + * string if the validator is currently validating the + * root value of the validation graph. + */ + public function getPropertyPath($subPath = null); +} diff --git a/src/Symfony/Component/Validator/GlobalExecutionContext.php b/src/Symfony/Component/Validator/GlobalExecutionContext.php deleted file mode 100644 index d60798fce9..0000000000 --- a/src/Symfony/Component/Validator/GlobalExecutionContext.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator; - -use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; - -/** - * Stores the node-independent information of a validation run. - * - * This class is immutable by design, except for violation tracking. - * - * @author Bernhard Schussek - */ -class GlobalExecutionContext -{ - private $root; - private $graphWalker; - private $metadataFactory; - private $violations; - - public function __construct($root, GraphWalker $graphWalker, ClassMetadataFactoryInterface $metadataFactory) - { - $this->root = $root; - $this->graphWalker = $graphWalker; - $this->metadataFactory = $metadataFactory; - $this->violations = new ConstraintViolationList(); - } - - public function __clone() - { - $this->violations = clone $this->violations; - } - - public function addViolation(ConstraintViolation $violation) - { - $this->violations->add($violation); - } - - /** - * @return ConstraintViolationList - */ - public function getViolations() - { - return $this->violations; - } - - public function getRoot() - { - return $this->root; - } - - /** - * @return GraphWalker - */ - public function getGraphWalker() - { - return $this->graphWalker; - } - - /** - * @return ClassMetadataFactoryInterface - */ - public function getMetadataFactory() - { - return $this->metadataFactory; - } -} diff --git a/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php b/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php new file mode 100644 index 0000000000..aff44b3507 --- /dev/null +++ b/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * Stores the node-independent state of a validation run. + * + * When the validator validates a graph of objects, it uses two classes to + * store the state during the validation: + * + *
    + *
  • For each node in the validation graph (objects, properties, getters) the + * validator creates an instance of {@link ExecutionContextInterface} that + * stores the information about that node.
  • + *
  • One single GlobalExecutionContextInterface stores the state + * that is independent of the current node.
  • + *
+ * + * @author Bernhard Schussek + */ +interface GlobalExecutionContextInterface +{ + /** + * Returns the violations generated by the validator so far. + * + * @return ConstraintViolationListInterface A list of constraint violations. + */ + public function getViolations(); + + /** + * Returns the value at which validation was started in the object graph. + * + * @return mixed The root value. + * + * @see ExecutionContextInterface::getRoot + */ + public function getRoot(); + + /** + * Returns the visitor instance used to validate the object graph nodes. + * + * @return ValidationVisitorInterface The validation visitor. + */ + public function getVisitor(); + + /** + * Returns the factory for constraint validators. + * + * @return ConstraintValidatorFactoryInterface The constraint validator factory. + */ + public function getValidatorFactory(); + + /** + * Returns the factory for validation metadata objects. + * + * @return MetadataFactoryInterface The metadata factory. + */ + public function getMetadataFactory(); +} diff --git a/src/Symfony/Component/Validator/GraphWalker.php b/src/Symfony/Component/Validator/GraphWalker.php index 93fcb9e286..26177b0929 100644 --- a/src/Symfony/Component/Validator/GraphWalker.php +++ b/src/Symfony/Component/Validator/GraphWalker.php @@ -11,10 +11,8 @@ namespace Symfony\Component\Validator; -use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\MemberMetadata; @@ -24,29 +22,52 @@ use Symfony\Component\Validator\Mapping\MemberMetadata; * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. This class + * has been replaced by {@link ValidationVisitorInterface} and + * {@link MetadataInterface}. */ class GraphWalker { - private $globalContext; - private $validatorFactory; - private $metadataFactory; - private $validatorInitializers = array(); - private $validatedObjects = array(); + /** + * @var ValidationVisitor + */ + private $visitor; - public function __construct($root, ClassMetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $factory, array $validatorInitializers = array()) + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var array + */ + private $validatedObjects; + + /** + * Creates a new graph walker. + * + * @param ValidationVisitor $visitor + * @param MetadataFactoryInterface $metadataFactory + * @param array $validatedObjects + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ + public function __construct(ValidationVisitor $visitor, MetadataFactoryInterface $metadataFactory, array &$validatedObjects = array()) { - $this->globalContext = new GlobalExecutionContext($root, $this, $metadataFactory); - $this->validatorFactory = $factory; + $this->visitor = $visitor; $this->metadataFactory = $metadataFactory; - $this->validatorInitializers = $validatorInitializers; + $this->validatedObjects = &$validatedObjects; } /** * @return ConstraintViolationList + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. */ public function getViolations() { - return $this->globalContext->getViolations(); + return $this->visitor->getViolations(); } /** @@ -57,128 +78,128 @@ class GraphWalker * @param object $object The object to validate * @param string $group The validator group to use for validation * @param string $propertyPath + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. */ public function walkObject(ClassMetadata $metadata, $object, $group, $propertyPath) - { - foreach ($this->validatorInitializers as $initializer) { - if (!$initializer instanceof ObjectInitializerInterface) { - throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); - } - $initializer->initialize($object); - } - - if ($group === Constraint::DEFAULT_GROUP && ($metadata->hasGroupSequence() || $metadata->isGroupSequenceProvider())) { - if ($metadata->hasGroupSequence()) { - $groups = $metadata->getGroupSequence(); - } else { - $groups = $object->getGroupSequence(); - } - - foreach ($groups as $group) { - $this->walkObjectForGroup($metadata, $object, $group, $propertyPath, Constraint::DEFAULT_GROUP); - - if (count($this->getViolations()) > 0) { - break; - } - } - } else { - $this->walkObjectForGroup($metadata, $object, $group, $propertyPath); - } - } - - protected function walkObjectForGroup(ClassMetadata $metadata, $object, $group, $propertyPath, $propagatedGroup = null) { $hash = spl_object_hash($object); // Exit, if the object is already validated for the current group if (isset($this->validatedObjects[$hash][$group])) { - return; + return; } // Remember validating this object before starting and possibly // traversing the object graph $this->validatedObjects[$hash][$group] = true; - $currentClass = $metadata->getClassName(); - - foreach ($metadata->findConstraints($group) as $constraint) { - $this->walkConstraint($constraint, $object, $group, $propertyPath, $currentClass); - } - - if (null !== $object) { - $pathPrefix = empty($propertyPath) ? '' : $propertyPath.'.'; - foreach ($metadata->getConstrainedProperties() as $property) { - $this->walkProperty($metadata, $property, $object, $group, $pathPrefix.$property, $propagatedGroup); - } - } + $metadata->accept($this->visitor, $object, $group, $propertyPath); } + protected function walkObjectForGroup(ClassMetadata $metadata, $object, $group, $propertyPath, $propagatedGroup = null) + { + $metadata->accept($this->visitor, $object, $group, $propertyPath, $propagatedGroup); + } + + /** + * Validates a property of a class. + * + * @param Mapping\ClassMetadata $metadata + * @param $property + * @param $object + * @param $group + * @param $propertyPath + * @param null $propagatedGroup + * + * @throws Exception\UnexpectedTypeException + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ public function walkProperty(ClassMetadata $metadata, $property, $object, $group, $propertyPath, $propagatedGroup = null) { + if (!is_object($object)) { + throw new UnexpectedTypeException($object, 'object'); + } + foreach ($metadata->getMemberMetadatas($property) as $member) { - $this->walkMember($member, $member->getValue($object), $group, $propertyPath, $propagatedGroup); + $member->accept($this->visitor, $member->getValue($object), $group, $propertyPath, $propagatedGroup); } } + /** + * Validates a property of a class against a potential value. + * + * @param Mapping\ClassMetadata $metadata + * @param $property + * @param $value + * @param $group + * @param $propertyPath + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ public function walkPropertyValue(ClassMetadata $metadata, $property, $value, $group, $propertyPath) { foreach ($metadata->getMemberMetadatas($property) as $member) { - $this->walkMember($member, $value, $group, $propertyPath); + $member->accept($this->visitor, $value, $group, $propertyPath); } } protected function walkMember(MemberMetadata $metadata, $value, $group, $propertyPath, $propagatedGroup = null) { - $currentClass = $metadata->getClassName(); - $currentProperty = $metadata->getPropertyName(); - - foreach ($metadata->findConstraints($group) as $constraint) { - $this->walkConstraint($constraint, $value, $group, $propertyPath, $currentClass, $currentProperty); - } - - if ($metadata->isCascaded()) { - $this->walkReference($value, $propagatedGroup ?: $group, $propertyPath, $metadata->isCollectionCascaded(), $metadata->isCollectionCascadedDeeply()); - } + $metadata->accept($this->visitor, $value, $group, $propertyPath, $propagatedGroup); } + /** + * Validates an object or an array. + * + * @param $value + * @param $group + * @param $propertyPath + * @param $traverse + * @param bool $deep + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ public function walkReference($value, $group, $propertyPath, $traverse, $deep = false) { - if (null !== $value) { - if (!is_object($value) && !is_array($value)) { - throw new UnexpectedTypeException($value, 'object or array'); - } - - if ($traverse && (is_array($value) || $value instanceof \Traversable)) { - foreach ($value as $key => $element) { - // Ignore any scalar values in the collection - if (is_object($element) || is_array($element)) { - // Only repeat the traversal if $deep is set - $this->walkReference($element, $group, $propertyPath.'['.$key.']', $deep, $deep); - } - } - } - - if (is_object($value)) { - $metadata = $this->metadataFactory->getClassMetadata(get_class($value)); - $this->walkObject($metadata, $value, $group, $propertyPath); - } - } + $this->visitor->validate($value, $group, $propertyPath, $traverse, $deep); } + /** + * Validates a value against a constraint. + * + * @param Constraint $constraint + * @param $value + * @param $group + * @param $propertyPath + * @param null $currentClass + * @param null $currentProperty + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ public function walkConstraint(Constraint $constraint, $value, $group, $propertyPath, $currentClass = null, $currentProperty = null) { - $validator = $this->validatorFactory->getInstance($constraint); + $metadata = null; - $localContext = new ExecutionContext( - $this->globalContext, + // BC code to make getCurrentClass() and getCurrentProperty() work when + // called from within this method + if (null !== $currentClass) { + $metadata = $this->metadataFactory->getMetadataFor($currentClass); + + if (null !== $currentProperty && $metadata instanceof PropertyMetadataContainerInterface) { + $metadata = current($metadata->getPropertyMetadata($currentProperty)); + } + } + + $context = new ExecutionContext( + $this->visitor, + $metadata, $value, - $propertyPath, $group, - $currentClass, - $currentProperty + $propertyPath ); - $validator->initialize($localContext); - $validator->validate($value, $constraint); + $context->validateValue($value, $constraint); } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 340d4d7414..f31bc95fc1 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\ValidationVisitorInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; +use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\MetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\GroupDefinitionException; @@ -21,7 +25,7 @@ use Symfony\Component\Validator\Exception\GroupDefinitionException; * @author Bernhard Schussek * @author Fabien Potencier */ -class ClassMetadata extends ElementMetadata +class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface { public $name; public $defaultGroup; @@ -47,6 +51,40 @@ class ClassMetadata extends ElementMetadata $this->defaultGroup = $class; } } + + public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath, $propagatedGroup = null) + { + if (null === $propagatedGroup && Constraint::DEFAULT_GROUP === $group + && ($this->hasGroupSequence() || $this->isGroupSequenceProvider())) { + if ($this->hasGroupSequence()) { + $groups = $this->getGroupSequence(); + } else { + $groups = $value->getGroupSequence(); + } + + foreach ($groups as $group) { + $this->accept($visitor, $value, $group, $propertyPath, Constraint::DEFAULT_GROUP); + + if (count($visitor->getViolations()) > 0) { + break; + } + } + + return; + } + + $visitor->visit($this, $value, $group, $propertyPath); + + if (null !== $value) { + $pathPrefix = empty($propertyPath) ? '' : $propertyPath.'.'; + + foreach ($this->getConstrainedProperties() as $property) { + foreach ($this->getMemberMetadatas($property) as $member) { + $member->accept($visitor, $member->getValue($value), $group, $pathPrefix.$property, $propagatedGroup); + } + } + } + } /** * Returns the properties to be serialized @@ -225,13 +263,21 @@ class ClassMetadata extends ElementMetadata * * @param string $property The name of the property * - * @return array An array of MemberMetadata + * @return MemberMetadata[] An array of MemberMetadata */ public function getMemberMetadatas($property) { return $this->members[$property]; } + /** + * {@inheritdoc} + */ + public function getPropertyMetadata($property) + { + return $this->members[$property]; + } + /** * Returns all properties for which constraints are defined. * diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index f84d7a3645..279d833455 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -11,15 +11,17 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; /** - * Implementation of ClassMetadataFactoryInterface + * A factory for creating metadata for PHP classes. * * @author Bernhard Schussek */ -class ClassMetadataFactory implements ClassMetadataFactoryInterface +class ClassMetadataFactory implements ClassMetadataFactoryInterface, MetadataFactoryInterface { /** * The loader for loading the class metadata @@ -41,9 +43,16 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface $this->cache = $cache; } - public function getClassMetadata($class) + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) { - $class = ltrim($class, '\\'); + if (!is_object($value) && !is_string($value)) { + throw new NoSuchMetadataException('Cannot create metadata for non-objects. Got: ' . gettype($value)); + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); if (isset($this->loadedClasses[$class])) { return $this->loadedClasses[$class]; @@ -53,6 +62,10 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface return $this->loadedClasses[$class]; } + if (!class_exists($class) && !interface_exists($class)) { + throw new NoSuchMetadataException('The class or interface "' . $class . '" does not exist.'); + } + $metadata = new ClassMetadata($class); // Include constraints from the parent class @@ -78,4 +91,33 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface return $this->loadedClasses[$class] = $metadata; } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + if (!is_object($value) && !is_string($value)) { + return false; + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); + + if (class_exists($class) || interface_exists($class)) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use + * {@link getMetadataFor} instead. + */ + public function getClassMetadata($class) + { + return $this->getMetadataFor($class); + } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryAdapter.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryAdapter.php new file mode 100644 index 0000000000..017e4e4f69 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryAdapter.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; + +/** + * An adapter for exposing {@link ClassMetadataFactoryInterface} implementations + * under the new {@link MetadataFactoryInterface}. + * + * @author Bernhard Schussek + */ +class ClassMetadataFactoryAdapter implements MetadataFactoryInterface +{ + /** + * @var ClassMetadataFactoryInterface + */ + private $innerFactory; + + public function __construct(ClassMetadataFactoryInterface $innerFactory) + { + $this->innerFactory = $innerFactory; + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + $class = is_object($value) ? get_class($value) : $value; + $metadata = $this->innerFactory->getClassMetadata($class); + + if (null === $metadata) { + throw new NoSuchMetadataException('No metadata exists for class '. $class); + } + + return $metadata; + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + $class = is_object($value) ? get_class($value) : $value; + + return null !== $this->innerFactory->getClassMetadata($class); + } +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryInterface.php index eb8c653370..42fff5c858 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactoryInterface.php @@ -11,7 +11,20 @@ namespace Symfony\Component\Validator\Mapping; +/** + * A factory for {@link ClassMetadata} objects. + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Implement + * {@link \Symfony\Component\Validator\MetadataFactoryInterface} instead. + */ interface ClassMetadataFactoryInterface { + /** + * Returns metadata for a given class. + * + * @param string $class The class name. + * + * @return ClassMetadata The class metadata instance. + */ public function getClassMetadata($class); } diff --git a/src/Symfony/Component/Validator/Mapping/GetterMetadata.php b/src/Symfony/Component/Validator/Mapping/GetterMetadata.php index fc8776f6f7..38cc6e4275 100644 --- a/src/Symfony/Component/Validator/Mapping/GetterMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GetterMetadata.php @@ -40,7 +40,7 @@ class GetterMetadata extends MemberMetadata /** * {@inheritDoc} */ - public function getValue($object) + public function getPropertyValue($object) { return $this->getReflectionMember()->invoke($object); } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 30d022d75f..7db22a84de 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,11 +11,14 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\ValidationVisitorInterface; +use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\PropertyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -abstract class MemberMetadata extends ElementMetadata +abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, ClassBasedInterface { public $class; public $name; @@ -39,6 +42,15 @@ abstract class MemberMetadata extends ElementMetadata $this->property = $property; } + public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath, $propagatedGroup = null) + { + $visitor->visit($this, $value, $group, $propertyPath); + + if ($this->isCascaded()) { + $visitor->validate($value, $propagatedGroup ?: $group, $propertyPath, $this->isCollectionCascaded(), $this->isCollectionCascadedDeeply()); + } + } + /** * {@inheritDoc} */ @@ -176,8 +188,14 @@ abstract class MemberMetadata extends ElementMetadata * @param object $object The object * * @return mixed The property value + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use the + * method {@link getPropertyValue} instead. */ - abstract public function getValue($object); + public function getValue($object) + { + return $this->getPropertyValue($object); + } /** * Returns the Reflection instance of the member diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php index 5e012ef6bb..caa7b6e144 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php @@ -33,7 +33,7 @@ class PropertyMetadata extends MemberMetadata /** * {@inheritDoc} */ - public function getValue($object) + public function getPropertyValue($object) { return $this->getReflectionMember()->getValue($object); } diff --git a/src/Symfony/Component/Validator/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/MetadataFactoryInterface.php new file mode 100644 index 0000000000..6dbab06ab7 --- /dev/null +++ b/src/Symfony/Component/Validator/MetadataFactoryInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * Returns {@link MetadataInterface} instances for values. + * + * @author Bernhard Schussek + */ +interface MetadataFactoryInterface +{ + /** + * Returns the metadata for the given value. + * + * @param mixed $value Some value. + * + * @return MetadataInterface The metadata for the value. + * + * @throws Exception\NoSuchMetadataException If no metadata exists for the value. + */ + public function getMetadataFor($value); + + /** + * Returns whether metadata exists for the given value. + * + * @param mixed $value Some value. + * + * @return Boolean Whether metadata exists for the value. + */ + public function hasMetadataFor($value); +} diff --git a/src/Symfony/Component/Validator/MetadataInterface.php b/src/Symfony/Component/Validator/MetadataInterface.php new file mode 100644 index 0000000000..a5d65048b7 --- /dev/null +++ b/src/Symfony/Component/Validator/MetadataInterface.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * A container for validation metadata. + * + * The container contains constraints that may belong to different validation + * groups. Constraints for a specific group can be fetched by calling + * {@link findConstraints}. + * + * Implement this interface to add validation metadata to your own metadata + * layer. Each metadata may have named properties. Each property can be + * represented by one or more {@link PropertyMetadataInterface} instances that + * are returned by {@link getPropertyMetadata}. Since + * PropertyMetadataInterface inherits from MetadataInterface, + * each property may be divided into further properties. + * + * The {@link accept} method of each metadata implements the Visitor pattern. + * The method should forward the call to the visitor's + * {@link ValidationVisitorInterface::visit} method and additionally call + * accept() on all structurally related metadata instances. + * + * For example, to store constraints for PHP classes and their properties, + * create a class ClassMetadata (implementing MetadataInterface) + * and a class PropertyMetadata (implementing PropertyMetadataInterface). + * ClassMetadata::getPropertyMetadata($property) returns all + * PropertyMetadata instances for a property of that class. Its + * accept()-method simply forwards to ValidationVisitorInterface::visit() + * and calls accept() on all contained PropertyMetadata + * instances, which themselves call ValidationVisitorInterface::visit() + * again. + * + * @author Bernhard Schussek + */ +interface MetadataInterface +{ + /** + * Implementation of the Visitor design pattern. + * + * Calls {@link ValidationVisitorInterface::visit} and then forwards the + * accept()-call to all property metadata instances. + * + * @param ValidationVisitorInterface $visitor The visitor implementing the validation logic. + * @param mixed $value The value to validate. + * @param string|string[] $group The validation group to validate in. + * @param string $propertyPath The current property path in the validation graph. + */ + public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath); + + /** + * Returns all constraints for a given validation group. + * + * @param string $group The validation group. + * + * @return Constraint[] A list of constraint instances. + */ + public function findConstraints($group); +} diff --git a/src/Symfony/Component/Validator/ObjectInitializerInterface.php b/src/Symfony/Component/Validator/ObjectInitializerInterface.php index 424ff0b624..0426bc8c80 100644 --- a/src/Symfony/Component/Validator/ObjectInitializerInterface.php +++ b/src/Symfony/Component/Validator/ObjectInitializerInterface.php @@ -12,12 +12,13 @@ namespace Symfony\Component\Validator; /** - * Interface for object initializers. + * Prepares an object for validation. * - * Concrete implementations of this interface are used by the GraphWalker - * to initialize objects just before validating them/ + * Concrete implementations of this interface are used by {@link ValidationVisitorInterface} + * to initialize objects just before validating them. * * @author Fabien Potencier + * @author Bernhard Schussek * * @api */ diff --git a/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php b/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php new file mode 100644 index 0000000000..33db49e4e1 --- /dev/null +++ b/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * A container for {@link PropertyMetadataInterface} instances. + * + * @author Bernhard Schussek + */ +interface PropertyMetadataContainerInterface +{ + /** + * Returns all metadata instances for the given named property. + * + * If your implementation does not support properties, simply throw an + * exception in this method (for example a BadMethodCallException). + * + * @param string $property The property name. + * + * @return PropertyMetadataInterface[] A list of metadata instances. Empty if + * no metadata exists for the property. + */ + public function getPropertyMetadata($property); +} diff --git a/src/Symfony/Component/Validator/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/PropertyMetadataInterface.php new file mode 100644 index 0000000000..eaac1a7121 --- /dev/null +++ b/src/Symfony/Component/Validator/PropertyMetadataInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * A container for validation metadata of a property. + * + * What exactly you define as "property" is up to you. The validator expects + * implementations of {@link MetadataInterface} that contain constraints and + * optionally a list of named properties that also have constraints (and may + * have further sub properties). Such properties are mapped by implementations + * of this interface. + * + * @author Bernhard Schussek + * + * @see MetadataInterface + */ +interface PropertyMetadataInterface extends MetadataInterface +{ + /** + * Returns the name of the property. + * + * @return string The property name. + */ + public function getPropertyName(); + + /** + * Extracts the value of the property from the given container. + * + * @param mixed $containingValue The container to extract the property value from. + * + * @return mixed The value of the property. + */ + public function getPropertyValue($containingValue); +} diff --git a/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php index bdde1a1c66..40b5996646 100644 --- a/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php @@ -11,27 +11,47 @@ namespace Symfony\Component\Validator\Tests; -use Symfony\Component\Validator\GlobalExecutionContext; - +use Symfony\Component\Validator\Mapping\PropertyMetadata; +use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\ConstraintViolation; - use Symfony\Component\Validator\ConstraintViolationList; - use Symfony\Component\Validator\ExecutionContext; class ExecutionContextTest extends \PHPUnit_Framework_TestCase { - protected $walker; - protected $metadataFactory; - protected $globalContext; - protected $context; + private $visitor; + private $violations; + private $metadata; + private $metadataFactory; + private $globalContext; + + /** + * @var ExecutionContext + */ + private $context; protected function setUp() { - $this->walker = $this->getMock('Symfony\Component\Validator\GraphWalker', array(), array(), '', false); - $this->metadataFactory = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface'); - $this->globalContext = new GlobalExecutionContext('Root', $this->walker, $this->metadataFactory); - $this->context = new ExecutionContext($this->globalContext, 'currentValue', 'foo.bar', 'Group', 'ClassName', 'propertyName'); + $this->visitor = $this->getMockBuilder('Symfony\Component\Validator\ValidationVisitor') + ->disableOriginalConstructor() + ->getMock(); + $this->violations = new ConstraintViolationList(); + $this->metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + $this->metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $this->globalContext = $this->getMock('Symfony\Component\Validator\GlobalExecutionContextInterface'); + $this->globalContext->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue('Root')); + $this->globalContext->expects($this->any()) + ->method('getViolations') + ->will($this->returnValue($this->violations)); + $this->globalContext->expects($this->any()) + ->method('getVisitor') + ->will($this->returnValue($this->visitor)); + $this->globalContext->expects($this->any()) + ->method('getMetadataFactory') + ->will($this->returnValue($this->metadataFactory)); + $this->context = new ExecutionContext($this->globalContext, $this->metadata, 'currentValue', 'Group', 'foo.bar'); } protected function tearDown() @@ -45,18 +65,48 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase $this->assertCount(0, $this->context->getViolations()); $this->assertSame('Root', $this->context->getRoot()); $this->assertSame('foo.bar', $this->context->getPropertyPath()); - $this->assertSame('ClassName', $this->context->getCurrentClass()); - $this->assertSame('propertyName', $this->context->getCurrentProperty()); $this->assertSame('Group', $this->context->getGroup()); - $this->assertSame($this->walker, $this->context->getGraphWalker()); + + $this->visitor->expects($this->once()) + ->method('getGraphWalker') + ->will($this->returnValue('GRAPHWALKER')); + + // BC + $this->assertNull($this->context->getCurrentClass()); + $this->assertNull($this->context->getCurrentProperty()); + $this->assertSame('GRAPHWALKER', $this->context->getGraphWalker()); $this->assertSame($this->metadataFactory, $this->context->getMetadataFactory()); } + public function testInitWithClassMetadata() + { + // BC + $this->metadata = new ClassMetadata(__NAMESPACE__ . '\ExecutionContextTest_TestClass'); + $this->context = new ExecutionContext($this->globalContext, $this->metadata, 'currentValue', 'Group', 'foo.bar'); + + $this->assertSame(__NAMESPACE__ . '\ExecutionContextTest_TestClass', $this->context->getCurrentClass()); + $this->assertNull($this->context->getCurrentProperty()); + } + + public function testInitWithPropertyMetadata() + { + // BC + $this->metadata = new PropertyMetadata(__NAMESPACE__ . '\ExecutionContextTest_TestClass', 'myProperty'); + $this->context = new ExecutionContext($this->globalContext, $this->metadata, 'currentValue', 'Group', 'foo.bar'); + + $this->assertSame(__NAMESPACE__ . '\ExecutionContextTest_TestClass', $this->context->getCurrentClass()); + $this->assertSame('myProperty', $this->context->getCurrentProperty()); + } + public function testClone() { $clone = clone $this->context; - $this->assertNotSame($this->context->getViolations(), $clone->getViolations()); + // Cloning the context keeps the reference to the original violation + // list. This way we can efficiently duplicate context instances during + // the validation run and only modify the properties that need to be + // changed. + $this->assertSame($this->context->getViolations(), $clone->getViolations()); } public function testAddViolation() @@ -170,10 +220,10 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase )), $this->context->getViolations()); } - public function testAddViolationAtSubPath() + public function testAddViolationAt() { // override preconfigured property path - $this->context->addViolationAtSubPath('bam.baz', 'Error', array('foo' => 'bar'), 'invalid'); + $this->context->addViolationAt('bam.baz', 'Error', array('foo' => 'bar'), 'invalid'); $this->assertEquals(new ConstraintViolationList(array( new ConstraintViolation( @@ -186,9 +236,9 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase )), $this->context->getViolations()); } - public function testAddViolationAtSubPathUsesPreconfiguredValueIfNotPassed() + public function testAddViolationAtUsesPreconfiguredValueIfNotPassed() { - $this->context->addViolationAtSubPath('bam.baz', 'Error'); + $this->context->addViolationAt('bam.baz', 'Error'); $this->assertEquals(new ConstraintViolationList(array( new ConstraintViolation( @@ -201,11 +251,11 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase )), $this->context->getViolations()); } - public function testAddViolationAtSubPathUsesPassedNullValue() + public function testAddViolationAtUsesPassedNullValue() { // passed null value should override preconfigured value "invalid" - $this->context->addViolationAtSubPath('bam.baz', 'Error', array('foo' => 'bar'), null); - $this->context->addViolationAtSubPath('bam.baz', 'Error', array('foo' => 'bar'), null, 1); + $this->context->addViolationAt('bam.baz', 'Error', array('foo' => 'bar'), null); + $this->context->addViolationAt('bam.baz', 'Error', array('foo' => 'bar'), null, 1); $this->assertEquals(new ConstraintViolationList(array( new ConstraintViolation( @@ -243,8 +293,13 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase public function testGetPropertyPathWithEmptyCurrentPropertyPath() { - $this->context = new ExecutionContext($this->globalContext, 'currentValue', '', 'Group', 'ClassName', 'propertyName'); + $this->context = new ExecutionContext($this->globalContext, $this->metadata, 'currentValue', 'Group', ''); $this->assertEquals('bam.baz', $this->context->getPropertyPath('bam.baz')); } } + +class ExecutionContextTest_TestClass +{ + public $myProperty; +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintAValidator.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintAValidator.php index 30fa92016c..787c78c691 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintAValidator.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintAValidator.php @@ -13,13 +13,13 @@ namespace Symfony\Component\Validator\Tests\Fixtures; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\ExecutionContext; +use Symfony\Component\Validator\ExecutionContextInterface; class ConstraintAValidator extends ConstraintValidator { public static $passedContext; - public function initialize(ExecutionContext $context) + public function initialize(ExecutionContextInterface $context) { parent::initialize($context); diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadataFactory.php deleted file mode 100644 index 644a35fc8f..0000000000 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadataFactory.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Fixtures; - -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; - -class FakeClassMetadataFactory implements ClassMetadataFactoryInterface -{ - protected $metadatas = array(); - - public function getClassMetadata($class) - { - if (!isset($this->metadatas[$class])) { - throw new \RuntimeException('No metadata for class ' . $class); - } - - return $this->metadatas[$class]; - } - - public function addClassMetadata(ClassMetadata $metadata) - { - $this->metadatas[$metadata->getClassName()] = $metadata; - } -} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php new file mode 100644 index 0000000000..1276ca5283 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Mapping\ClassMetadata; + +class FakeMetadataFactory implements MetadataFactoryInterface +{ + protected $metadatas = array(); + + public function getMetadataFor($class) + { + if (is_object($class)) { + $class = get_class($class); + } + + if (!is_string($class)) { + throw new NoSuchMetadataException('No metadata for type ' . gettype($class)); + } + + if (!isset($this->metadatas[$class])) { + throw new NoSuchMetadataException('No metadata for "' . $class . '"'); + } + + return $this->metadatas[$class]; + } + + public function hasMetadataFor($class) + { + if (is_object($class)) { + $class = get_class($class); + } + + if (!is_string($class)) { + return false; + } + + return isset($this->metadatas[$class]); + } + + public function addMetadata(ClassMetadata $metadata) + { + $this->metadatas[$metadata->getClassName()] = $metadata; + } +} diff --git a/src/Symfony/Component/Validator/Tests/GlobalExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/GlobalExecutionContextTest.php deleted file mode 100644 index cadb28c634..0000000000 --- a/src/Symfony/Component/Validator/Tests/GlobalExecutionContextTest.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests; - -use Symfony\Component\Validator\ConstraintViolation; - -use Symfony\Component\Validator\ConstraintViolationList; - -use Symfony\Component\Validator\GlobalExecutionContext; - -class GlobalExecutionContextTest extends \PHPUnit_Framework_TestCase -{ - protected $walker; - protected $metadataFactory; - protected $context; - - protected function setUp() - { - $this->walker = $this->getMock('Symfony\Component\Validator\GraphWalker', array(), array(), '', false); - $this->metadataFactory = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface'); - $this->context = new GlobalExecutionContext('Root', $this->walker, $this->metadataFactory); - } - - protected function tearDown() - { - $this->walker = null; - $this->metadataFactory = null; - $this->context = null; - } - - public function testInit() - { - $this->assertCount(0, $this->context->getViolations()); - $this->assertSame('Root', $this->context->getRoot()); - $this->assertSame($this->walker, $this->context->getGraphWalker()); - $this->assertSame($this->metadataFactory, $this->context->getMetadataFactory()); - } - - public function testClone() - { - $clone = clone $this->context; - - $this->assertNotSame($this->context->getViolations(), $clone->getViolations()); - } - - public function testAddViolation() - { - $violation = new ConstraintViolation('Error', array(), 'Root', 'foo.bar', 'invalid'); - - $this->context->addViolation($violation); - - $this->assertEquals(new ConstraintViolationList(array($violation)), $this->context->getViolations()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php b/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php index 5fd8b186c7..83a67db3cd 100644 --- a/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php +++ b/src/Symfony/Component/Validator/Tests/GraphWalkerTest.php @@ -12,10 +12,10 @@ namespace Symfony\Component\Validator\Tests; use Symfony\Component\Validator\Tests\Fixtures\ConstraintAValidator; - +use Symfony\Component\Validator\ValidationVisitor; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\Reference; -use Symfony\Component\Validator\Tests\Fixtures\FakeClassMetadataFactory; +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; use Symfony\Component\Validator\GraphWalker; @@ -30,20 +30,39 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { const CLASSNAME = 'Symfony\Component\Validator\Tests\Fixtures\Entity'; - protected $factory; + /** + * @var ValidationVisitor + */ + private $visitor; + + /** + * @var FakeMetadataFactory + */ + protected $metadataFactory; + + /** + * @var GraphWalker + */ protected $walker; + + /** + * @var ClassMetadata + */ protected $metadata; protected function setUp() { - $this->factory = new FakeClassMetadataFactory(); - $this->walker = new GraphWalker('Root', $this->factory, new ConstraintValidatorFactory()); + $this->metadataFactory = new FakeMetadataFactory(); + $this->visitor = new ValidationVisitor('Root', $this->metadataFactory, new ConstraintValidatorFactory()); + $this->walker = $this->visitor->getGraphWalker(); $this->metadata = new ClassMetadata(self::CLASSNAME); + $this->metadataFactory->addMetadata($this->metadata); } protected function tearDown() { - $this->factory = null; + $this->metadataFactory = null; + $this->visitor = null; $this->walker = null; $this->metadata = null; } @@ -82,6 +101,18 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase $this->assertCount(1, $this->walker->getViolations()); } + public function testWalkObjectOnceInVisitorAndOnceInWalkerValidatesConstraintsOnce() + { + $this->metadata->addConstraint(new ConstraintA()); + + $entity = new Entity(); + + $this->visitor->validate($entity, 'Default', ''); + $this->walker->walkObject($this->metadata, $entity, 'Default', ''); + + $this->assertCount(1, $this->walker->getViolations()); + } + public function testWalkDifferentObjectsValidatesTwice() { $this->metadata->addConstraint(new ConstraintA()); @@ -165,7 +196,7 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase // propagated to the reference 'groups' => 'Default', ))); - $this->factory->addClassMetadata($referenceMetadata); + $this->metadataFactory->addMetadata($referenceMetadata); $this->walker->walkObject($this->metadata, $entity, 'Default', ''); @@ -235,7 +266,7 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $entity = new Entity(); $entityMetadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($entityMetadata); + $this->metadataFactory->addMetadata($entityMetadata); // add a constraint for the entity that always fails $entityMetadata->addConstraint(new FailingConstraint()); @@ -268,7 +299,7 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $entity = new Entity(); $entityMetadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($entityMetadata); + $this->metadataFactory->addMetadata($entityMetadata); // add a constraint for the entity that always fails $entityMetadata->addConstraint(new FailingConstraint()); @@ -300,8 +331,8 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $entity = new Entity(); $entityMetadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($entityMetadata); - $this->factory->addClassMetadata(new ClassMetadata('ArrayIterator')); + $this->metadataFactory->addMetadata($entityMetadata); + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); // add a constraint for the entity that always fails $entityMetadata->addConstraint(new FailingConstraint()); @@ -333,8 +364,8 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $entity = new Entity(); $entityMetadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($entityMetadata); - $this->factory->addClassMetadata(new ClassMetadata('ArrayIterator')); + $this->metadataFactory->addMetadata($entityMetadata); + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); // add a constraint for the entity that always fails $entityMetadata->addConstraint(new FailingConstraint()); @@ -361,8 +392,8 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $entity = new Entity(); $entityMetadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($entityMetadata); - $this->factory->addClassMetadata(new ClassMetadata('ArrayIterator')); + $this->metadataFactory->addMetadata($entityMetadata); + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); // add a constraint for the entity that always fails $entityMetadata->addConstraint(new FailingConstraint()); @@ -392,8 +423,8 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $entity = new Entity(); $entityMetadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($entityMetadata); - $this->factory->addClassMetadata(new ClassMetadata('ArrayIterator')); + $this->metadataFactory->addMetadata($entityMetadata); + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); // add a constraint for the entity that always fails $entityMetadata->addConstraint(new FailingConstraint()); @@ -465,7 +496,7 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase { $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->setExpectedException('Symfony\Component\Validator\Exception\UnexpectedTypeException'); + $this->setExpectedException('Symfony\Component\Validator\Exception\NoSuchMetadataException'); $this->walker->walkPropertyValue( $this->metadata, diff --git a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php index 710178b37c..747a36b39a 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php @@ -73,7 +73,7 @@ class MemberMetadataTest extends \PHPUnit_Framework_TestCase class TestMemberMetadata extends MemberMetadata { - public function getValue($object) + public function getPropertyValue($object) { } diff --git a/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php b/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php new file mode 100644 index 0000000000..ab35b6f799 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php @@ -0,0 +1,508 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests; + +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Tests\Fixtures\Reference; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintAValidator; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\ValidationVisitor; + +/** + * @author Bernhard Schussek + */ +class ValidationVisitorTest extends \PHPUnit_Framework_TestCase +{ + const CLASS_NAME = 'Symfony\Component\Validator\Tests\Fixtures\Entity'; + + /** + * @var ValidationVisitor + */ + private $visitor; + + /** + * @var FakeMetadataFactory + */ + private $metadataFactory; + + /** + * @var ClassMetadata + */ + private $metadata; + + protected function setUp() + { + $this->metadataFactory = new FakeMetadataFactory(); + $this->visitor = new ValidationVisitor('Root', $this->metadataFactory, new ConstraintValidatorFactory()); + $this->metadata = new ClassMetadata(self::CLASS_NAME); + $this->metadataFactory->addMetadata($this->metadata); + } + + protected function tearDown() + { + $this->metadataFactory = null; + $this->visitor = null; + $this->metadata = null; + } + + public function testValidatePassesCorrectClassAndProperty() + { + $this->metadata->addConstraint(new ConstraintA()); + + $entity = new Entity(); + $this->visitor->validate($entity, 'Default', ''); + + $context = ConstraintAValidator::$passedContext; + + $this->assertEquals('Symfony\Component\Validator\Tests\Fixtures\Entity', $context->getClassName()); + $this->assertNull($context->getPropertyName()); + } + + public function testValidateConstraints() + { + $this->metadata->addConstraint(new ConstraintA()); + + $this->visitor->validate(new Entity(), 'Default', ''); + + $this->assertCount(1, $this->visitor->getViolations()); + } + + public function testValidateTwiceValidatesConstraintsOnce() + { + $this->metadata->addConstraint(new ConstraintA()); + + $entity = new Entity(); + + $this->visitor->validate($entity, 'Default', ''); + $this->visitor->validate($entity, 'Default', ''); + + $this->assertCount(1, $this->visitor->getViolations()); + } + + public function testValidateDifferentObjectsValidatesTwice() + { + $this->metadata->addConstraint(new ConstraintA()); + + $this->visitor->validate(new Entity(), 'Default', ''); + $this->visitor->validate(new Entity(), 'Default', ''); + + $this->assertCount(2, $this->visitor->getViolations()); + } + + public function testValidateTwiceInDifferentGroupsValidatesTwice() + { + $this->metadata->addConstraint(new ConstraintA()); + $this->metadata->addConstraint(new ConstraintA(array('groups' => 'Custom'))); + + $entity = new Entity(); + + $this->visitor->validate($entity, 'Default', ''); + $this->visitor->validate($entity, 'Custom', ''); + + $this->assertCount(2, $this->visitor->getViolations()); + } + + public function testValidatePropertyConstraints() + { + $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); + + $this->visitor->validate(new Entity(), 'Default', ''); + + $this->assertCount(1, $this->visitor->getViolations()); + } + + public function testValidateGetterConstraints() + { + $this->metadata->addGetterConstraint('lastName', new ConstraintA()); + + $this->visitor->validate(new Entity(), 'Default', ''); + + $this->assertCount(1, $this->visitor->getViolations()); + } + + public function testValidateInDefaultGroupTraversesGroupSequence() + { + $entity = new Entity(); + + $this->metadata->addPropertyConstraint('firstName', new FailingConstraint(array( + 'groups' => 'First', + ))); + $this->metadata->addGetterConstraint('lastName', new FailingConstraint(array( + 'groups' => 'Default', + ))); + $this->metadata->setGroupSequence(array('First', $this->metadata->getDefaultGroup())); + + $this->visitor->validate($entity, 'Default', ''); + + // After validation of group "First" failed, no more group was + // validated + $violations = new ConstraintViolationList(array( + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'firstName', + '' + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateInGroupSequencePropagatesDefaultGroup() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->setGroupSequence(array($this->metadata->getDefaultGroup())); + + $referenceMetadata = new ClassMetadata(get_class($entity->reference)); + $referenceMetadata->addConstraint(new FailingConstraint(array( + // this constraint is only evaluated if group "Default" is + // propagated to the reference + 'groups' => 'Default', + ))); + $this->metadataFactory->addMetadata($referenceMetadata); + + $this->visitor->validate($entity, 'Default', ''); + + // The validation of the reference's FailingConstraint in group + // "Default" was launched + $violations = new ConstraintViolationList(array( + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'reference', + $entity->reference + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateInOtherGroupTraversesNoGroupSequence() + { + $entity = new Entity(); + + $this->metadata->addPropertyConstraint('firstName', new FailingConstraint(array( + 'groups' => 'First', + ))); + $this->metadata->addGetterConstraint('lastName', new FailingConstraint(array( + 'groups' => $this->metadata->getDefaultGroup(), + ))); + $this->metadata->setGroupSequence(array('First', $this->metadata->getDefaultGroup())); + + $this->visitor->validate($entity, $this->metadata->getDefaultGroup(), ''); + + // Only group "Second" was validated + $violations = new ConstraintViolationList(array( + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'lastName', + '' + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyValidatesReferences() + { + $entity = new Entity(); + $entity->reference = new Entity(); + + // add a constraint for the entity that always fails + $this->metadata->addConstraint(new FailingConstraint()); + + // validate entity when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid()); + + // invoke validation on an object + $this->visitor->validate($entity, 'Default', ''); + + $violations = new ConstraintViolationList(array( + // generated by the root object + new ConstraintViolation( + 'Failed', + array(), + 'Root', + '', + $entity + ), + // generated by the reference + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'reference', + $entity->reference + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyValidatesArraysByDefault() + { + $entity = new Entity(); + $entity->reference = array('key' => new Entity()); + + // add a constraint for the entity that always fails + $this->metadata->addConstraint(new FailingConstraint()); + + // validate array when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->visitor->validate($entity, 'Default', ''); + + $violations = new ConstraintViolationList(array( + // generated by the root object + new ConstraintViolation( + 'Failed', + array(), + 'Root', + '', + $entity + ), + // generated by the reference + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'reference[key]', + $entity->reference['key'] + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyValidatesTraversableByDefault() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Entity())); + + // add a constraint for the entity that always fails + $this->metadata->addConstraint(new FailingConstraint()); + + // validate array when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->visitor->validate($entity, 'Default', ''); + + $violations = new ConstraintViolationList(array( + // generated by the root object + new ConstraintViolation( + 'Failed', + array(), + 'Root', + '', + $entity + ), + // generated by the reference + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'reference[key]', + $entity->reference['key'] + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyDoesNotValidateTraversableIfDisabled() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Entity())); + + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + + // add a constraint for the entity that always fails + $this->metadata->addConstraint(new FailingConstraint()); + + // validate array when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + + $this->visitor->validate($entity, 'Default', ''); + + $violations = new ConstraintViolationList(array( + // generated by the root object + new ConstraintViolation( + 'Failed', + array(), + 'Root', + '', + $entity + ), + // nothing generated by the reference! + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testMetadataMayNotExistIfTraversalIsEnabled() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(); + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => true, + ))); + + $this->visitor->validate($entity, 'Default', ''); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testMetadataMustExistIfTraversalIsDisabled() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(); + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + + $this->visitor->validate($entity, 'Default', ''); + } + + public function testValidateCascadedPropertyDoesNotRecurseByDefault() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + // The inner iterator should not be traversed by default + 'key' => new \ArrayIterator(array( + 'nested' => new Entity(), + )), + )); + + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + + // add a constraint for the entity that always fails + $this->metadata->addConstraint(new FailingConstraint()); + + // validate iterator when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->visitor->validate($entity, 'Default', ''); + + $violations = new ConstraintViolationList(array( + // generated by the root object + new ConstraintViolation( + 'Failed', + array(), + 'Root', + '', + $entity + ), + // nothing generated by the reference! + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyRecursesIfDeepIsSet() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + // The inner iterator should now be traversed + 'key' => new \ArrayIterator(array( + 'nested' => new Entity(), + )), + )); + + // add a constraint for the entity that always fails + $this->metadata->addConstraint(new FailingConstraint()); + + // validate iterator when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => true, + ))); + + $this->visitor->validate($entity, 'Default', ''); + + $violations = new ConstraintViolationList(array( + // generated by the root object + new ConstraintViolation( + 'Failed', + array(), + 'Root', + '', + $entity + ), + // nothing generated by the reference! + new ConstraintViolation( + 'Failed', + array(), + 'Root', + 'reference[key][nested]', + $entity->reference['key']['nested'] + ), + )); + + $this->assertEquals($violations, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyDoesNotValidateNestedScalarValues() + { + $entity = new Entity(); + $entity->reference = array('scalar', 'values'); + + // validate array when validating the property "reference" + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->visitor->validate($entity, 'Default', ''); + + $this->assertCount(0, $this->visitor->getViolations()); + } + + public function testValidateCascadedPropertyDoesNotValidateNullValues() + { + $entity = new Entity(); + $entity->reference = null; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->visitor->validate($entity, 'Default', ''); + + $this->assertCount(0, $this->visitor->getViolations()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testValidateCascadedPropertyRequiresObjectOrArray() + { + $entity = new Entity(); + $entity->reference = 'no object'; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->visitor->validate($entity, 'Default', ''); + } +} diff --git a/src/Symfony/Component/Validator/Tests/ValidatorContextTest.php b/src/Symfony/Component/Validator/Tests/ValidatorContextTest.php index 85d43b0af2..e10c2dad96 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorContextTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorContextTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests; use Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Mapping\ClassMetadataFactoryAdapter; use Symfony\Component\Validator\ValidatorContext; class ValidatorContextTest extends \PHPUnit_Framework_TestCase @@ -56,6 +57,6 @@ class ValidatorContextTest extends \PHPUnit_Framework_TestCase ->setConstraintValidatorFactory($validatorFactory) ->getValidator(); - $this->assertEquals(new Validator($metadataFactory, $validatorFactory), $validator); + $this->assertEquals(new Validator(new ClassMetadataFactoryAdapter($metadataFactory), $validatorFactory), $validator); } } diff --git a/src/Symfony/Component/Validator/Tests/ValidatorFactoryTest.php b/src/Symfony/Component/Validator/Tests/ValidatorFactoryTest.php index cafafb8a07..8ca3758826 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests; use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\Validator\Mapping\ClassMetadataFactoryAdapter; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\ValidatorContext; use Symfony\Component\Validator\ValidatorFactory; @@ -77,7 +78,7 @@ class ValidatorFactoryTest extends \PHPUnit_Framework_TestCase $validator = $this->factory->getValidator(); - $this->assertEquals(new Validator($metadataFactory, $validatorFactory), $validator); + $this->assertEquals(new Validator(new ClassMetadataFactoryAdapter($metadataFactory), $validatorFactory), $validator); } public function testBuildDefaultFromAnnotationsWithCustomNamespaces() diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php index 20398e4e52..a2fccc684a 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Validator\Tests; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; -use Symfony\Component\Validator\Tests\Fixtures\FakeClassMetadataFactory; +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\ConstraintViolation; @@ -24,18 +24,25 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; class ValidatorTest extends \PHPUnit_Framework_TestCase { - protected $factory; - protected $validator; + /** + * @var FakeMetadataFactory + */ + private $metadataFactory; + + /** + * @var Validator + */ + private $validator; protected function setUp() { - $this->factory = new FakeClassMetadataFactory(); - $this->validator = new Validator($this->factory, new ConstraintValidatorFactory()); + $this->metadataFactory = new FakeMetadataFactory(); + $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory()); } protected function tearDown() { - $this->factory = null; + $this->metadataFactory = null; $this->validator = null; } @@ -47,7 +54,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( 'groups' => 'Custom', ))); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); // Only the constraint of group "Default" failed $violations = new ConstraintViolationList(); @@ -70,7 +77,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( 'groups' => 'Custom', ))); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); // Only the constraint of group "Custom" failed $violations = new ConstraintViolationList(); @@ -95,7 +102,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( 'groups' => 'Second', ))); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); // The constraints of both groups failed $violations = new ConstraintViolationList(); @@ -130,7 +137,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase 'groups' => 'Second', ))); $metadata->setGroupSequenceProvider(true); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); $violations = new ConstraintViolationList(); $violations->add(new ConstraintViolation( @@ -168,7 +175,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $entity = new Entity(); $metadata = new ClassMetadata(get_class($entity)); $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); $result = $this->validator->validateProperty($entity, 'firstName'); @@ -180,7 +187,7 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $entity = new Entity(); $metadata = new ClassMetadata(get_class($entity)); $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); $result = $this->validator->validatePropertyValue(get_class($entity), 'firstName', 'Bernhard'); @@ -202,21 +209,55 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase } /** - * @expectedException Symfony\Component\Validator\Exception\ValidatorException + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ public function testValidateValueRejectsValid() { $entity = new Entity(); $metadata = new ClassMetadata(get_class($entity)); - $this->factory->addClassMetadata($metadata); + $this->metadataFactory->addMetadata($metadata); $this->validator->validateValue($entity, new Valid()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidatePropertyFailsIfPropertiesNotSupported() + { + // $metadata does not implement PropertyMetadataContainerInterface + $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + $this->metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $this->metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->with('VALUE') + ->will($this->returnValue($metadata)); + $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory()); + + $this->validator->validateProperty('VALUE', 'someProperty'); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidatePropertyValueFailsIfPropertiesNotSupported() + { + // $metadata does not implement PropertyMetadataContainerInterface + $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + $this->metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $this->metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->with('VALUE') + ->will($this->returnValue($metadata)); + $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory()); + + $this->validator->validatePropertyValue('VALUE', 'someProperty', 'propertyValue'); + } + public function testGetMetadataFactory() { $this->assertInstanceOf( - 'Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface', + 'Symfony\Component\Validator\MetadataFactoryInterface', $this->validator->getMetadataFactory() ); } diff --git a/src/Symfony/Component/Validator/ValidationVisitor.php b/src/Symfony/Component/Validator/ValidationVisitor.php new file mode 100644 index 0000000000..c8268304cd --- /dev/null +++ b/src/Symfony/Component/Validator/ValidationVisitor.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +/** + * Default implementation of {@link ValidationVisitorInterface} and + * {@link GlobalExecutionContextInterface}. + * + * @author Bernhard Schussek + */ +class ValidationVisitor implements ValidationVisitorInterface, GlobalExecutionContextInterface +{ + /** + * @var mixed + */ + private $root; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var ConstraintValidatorFactoryInterface + */ + private $validatorFactory; + + /** + * @var array + */ + private $objectInitializers; + + /** + * @var ConstraintViolationList + */ + private $violations; + + /** + * @var array + */ + private $validatedObjects = array(); + + /** + * @var GraphWalker + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ + private $graphWalker; + + /** + * Creates a new validation visitor. + * + * @param mixed $root The value passed to the validator. + * @param MetadataFactoryInterface $metadataFactory The factory for obtaining metadata instances. + * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating constraint validators. + * @param ObjectInitializerInterface[] $objectInitializers The initializers for preparing objects before validation. + * + * @throws UnexpectedTypeException If any of the object initializers is not an instance of ObjectInitializerInterface + */ + public function __construct($root, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array()) + { + foreach ($objectInitializers as $initializer) { + if (!$initializer instanceof ObjectInitializerInterface) { + throw new UnexpectedTypeException($initializer, 'Symfony\Component\Validator\ObjectInitializerInterface'); + } + } + + $this->root = $root; + $this->metadataFactory = $metadataFactory; + $this->validatorFactory = $validatorFactory; + $this->objectInitializers = $objectInitializers; + $this->violations = new ConstraintViolationList(); + } + + /** + * {@inheritdoc} + */ + public function visit(MetadataInterface $metadata, $value, $group, $propertyPath) + { + $context = new ExecutionContext( + $this, + $metadata, + $value, + $group, + $propertyPath + ); + + $context->validateValue($value, $metadata->findConstraints($group)); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $group, $propertyPath, $traverse = false, $deep = false) + { + if (null === $value) { + return; + } + + if (is_object($value)) { + $hash = spl_object_hash($value); + + // Exit, if the object is already validated for the current group + if (isset($this->validatedObjects[$hash][$group])) { + return; + } + + // Remember validating this object before starting and possibly + // traversing the object graph + $this->validatedObjects[$hash][$group] = true; + + foreach ($this->objectInitializers as $initializer) { + if (!$initializer instanceof ObjectInitializerInterface) { + throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); + } + $initializer->initialize($value); + } + } + + if ($traverse && (is_array($value) || $value instanceof \Traversable)) { + foreach ($value as $key => $element) { + // Ignore any scalar values in the collection + if (is_object($element) || is_array($element)) { + // Only repeat the traversal if $deep is set + $this->validate($element, $group, $propertyPath.'['.$key.']', $deep, $deep); + } + } + + try { + $this->metadataFactory->getMetadataFor($value)->accept($this, $value, $group, $propertyPath); + } catch (NoSuchMetadataException $e) { + // Metadata doesn't necessarily have to exist for + // traversable objects, because we know how to validate + // them anyway. Optionally, additional metadata is supported. + } + } else { + $this->metadataFactory->getMetadataFor($value)->accept($this, $value, $group, $propertyPath); + } + } + + /** + * {@inheritdoc} + */ + public function getGraphWalker() + { + if (null === $this->graphWalker) { + $this->graphWalker = new GraphWalker($this, $this->metadataFactory, $this->validatedObjects); + } + + return $this->graphWalker; + } + + /** + * {@inheritdoc} + */ + public function getViolations() + { + return $this->violations; + } + + /** + * {@inheritdoc} + */ + public function getRoot() + { + return $this->root; + } + + /** + * {@inheritdoc} + */ + public function getVisitor() + { + return $this; + } + + /** + * {@inheritdoc} + */ + public function getValidatorFactory() + { + return $this->validatorFactory; + } + + /** + * {@inheritdoc} + */ + public function getMetadataFactory() + { + return $this->metadataFactory; + } +} diff --git a/src/Symfony/Component/Validator/ValidationVisitorInterface.php b/src/Symfony/Component/Validator/ValidationVisitorInterface.php new file mode 100644 index 0000000000..fb1022ffd6 --- /dev/null +++ b/src/Symfony/Component/Validator/ValidationVisitorInterface.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * Validates values against constraints defined in {@link MetadataInterface} + * instances. + * + * This interface is an implementation of the Visitor design pattern. A value + * is validated by first passing it to the {@link validate} method. That method + * will determine the matching {@link MetadataInterface} for validating the + * value. It then calls the {@link MetadataInterface::accept} method of that + * metadata. accept() does two things: + * + *
    + *
  1. It calls {@link visit} to validate the value against the constraints of + * the metadata.
  2. + *
  3. It calls accept() on all nested metadata instances with the + * corresponding values extracted from the current value. For example, if the + * current metadata represents a class and the current value is an object of + * that class, the metadata contains nested instances for each property of that + * class. It forwards the call to these nested metadata with the values of the + * corresponding properties in the original object.
  4. + *
+ * + * @author Bernhard Schussek + */ +interface ValidationVisitorInterface +{ + /** + * Validates a value. + * + * If the value is an array or a traversable object, you can set the + * parameter $traverse to true in order to run through + * the collection and validate each element. If these elements can be + * collections again and you want to traverse them recursively, set the + * parameter $deep to true as well. + * + * If you set $traversable to true, the visitor will + * nevertheless try to find metadata for the collection and validate its + * constraints. If no such metadata is found, the visitor ignores that and + * only iterates the collection. + * + * If you don't set $traversable to true and the visitor + * does not find metadata for the given value, it will fail with an + * exception. + * + * @param mixed $value The value to validate. + * @param string $group The validation group to validate. + * @param string $propertyPath The current property path in the validation graph. + * @param Boolean $traverse Whether to traverse the value if it is traversable. + * @param Boolean $deep Whether to traverse nested traversable values recursively. + * + * @throws Exception\NoSuchMetadataException If no metadata can be found for + * the given value. + */ + public function validate($value, $group, $propertyPath, $traverse = false, $deep = false); + + /** + * Validates a value against the constraints defined in some metadata. + * + * This method implements the Visitor design pattern. See also + * {@link ValidationVisitorInterface}. + * + * @param MetadataInterface $metadata The metadata holding the constraints. + * @param mixed $value The value to validate. + * @param string $group The validation group to validate. + * @param string $propertyPath The current property path in the validation graph. + */ + public function visit(MetadataInterface $metadata, $value, $group, $propertyPath); + + /** + * Returns a graph walker with an alternative, deprecated API of the + * visitor. + * + * @return GraphWalker The graph walker. + * + * @deprecated Deprecated since version 2.2, to be removed in 2.3. + */ + public function getGraphWalker(); +} diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index c6e1fafe2e..848dc4e2ed 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -12,39 +12,44 @@ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; use Symfony\Component\Validator\Exception\ValidatorException; /** - * The default implementation of the ValidatorInterface. - * - * This service can be used to validate objects, properties and raw values - * against constraints. + * Default implementation of {@link ValidatorInterface}. * * @author Fabien Potencier * @author Bernhard Schussek - * - * @api */ class Validator implements ValidatorInterface { - protected $metadataFactory; - protected $validatorFactory; - protected $validatorInitializers; + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var ConstraintValidatorFactoryInterface + */ + private $validatorFactory; + + /** + * @var array + */ + private $objectInitializers; public function __construct( - ClassMetadataFactoryInterface $metadataFactory, + MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, - array $validatorInitializers = array() + array $objectInitializers = array() ) { $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; - $this->validatorInitializers = $validatorInitializers; + $this->objectInitializers = $objectInitializers; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadataFactory() { @@ -53,91 +58,134 @@ class Validator implements ValidatorInterface /** * {@inheritDoc} - * - * @api */ - public function validate($object, $groups = null) + public function getMetadataFor($value) { - $metadata = $this->metadataFactory->getClassMetadata(get_class($object)); - - $walk = function(GraphWalker $walker, $group) use ($metadata, $object) { - return $walker->walkObject($metadata, $object, $group, ''); - }; - - return $this->validateGraph($object, $walk, $groups); + return $this->metadataFactory->getMetadataFor($value); } /** * {@inheritDoc} - * - * @api */ - public function validateProperty($object, $property, $groups = null) + public function validate($value, $groups = null, $traverse = false, $deep = false) { - $metadata = $this->metadataFactory->getClassMetadata(get_class($object)); + $visitor = $this->createVisitor($value); - $walk = function(GraphWalker $walker, $group) use ($metadata, $property, $object) { - return $walker->walkProperty($metadata, $property, $object, $group, ''); - }; - - return $this->validateGraph($object, $walk, $groups); - } - - /** - * {@inheritDoc} - * - * @api - */ - public function validatePropertyValue($class, $property, $value, $groups = null) - { - $metadata = $this->metadataFactory->getClassMetadata($class); - - $walk = function(GraphWalker $walker, $group) use ($metadata, $property, $value) { - return $walker->walkPropertyValue($metadata, $property, $value, $group, ''); - }; - - return $this->validateGraph($class, $walk, $groups); - } - - /** - * {@inheritDoc} - * - * @api - */ - public function validateValue($value, Constraint $constraint, $groups = null) - { - if ($constraint instanceof Valid) { - // Why can't the Valid constraint be executed directly? - // - // It cannot be executed like regular other constraints, because regular - // constraints are only executed *if they belong to the validated group*. - // The Valid constraint, on the other hand, is always executed and propagates - // the group to the cascaded object. The propagated group depends on - // - // * Whether a group sequence is currently being executed. Then the default - // group is propagated. - // - // * Otherwise the validated group is propagated. - - throw new ValidatorException('The constraint ' . get_class($constraint) . ' cannot be validated. Use the method validate() instead.'); + foreach ($this->resolveGroups($groups) as $group) { + $visitor->validate($value, $group, ''); } - $walk = function(GraphWalker $walker, $group) use ($constraint, $value) { - return $walker->walkConstraint($constraint, $value, $group, ''); - }; - - return $this->validateGraph('', $walk, $groups); + return $visitor->getViolations(); } - protected function validateGraph($root, \Closure $walk, $groups = null) + /** + * {@inheritDoc} + * + * @throws ValidatorException If the metadata for the value does not support properties. + */ + public function validateProperty($containingValue, $property, $groups = null) { - $walker = new GraphWalker($root, $this->metadataFactory, $this->validatorFactory, $this->validatorInitializers); - $groups = $groups ? (array) $groups : array(Constraint::DEFAULT_GROUP); + $visitor = $this->createVisitor($containingValue); + $metadata = $this->metadataFactory->getMetadataFor($containingValue); - foreach ($groups as $group) { - $walk($walker, $group); + if (!$metadata instanceof PropertyMetadataContainerInterface) { + $valueAsString = is_scalar($containingValue) + ? '"' . $containingValue . '"' + : 'the value of type ' . gettype($containingValue); + + throw new ValidatorException(sprintf('The metadata for ' . $valueAsString . ' does not support properties.')); } - return $walker->getViolations(); + foreach ($this->resolveGroups($groups) as $group) { + foreach ($metadata->getPropertyMetadata($property) as $propMeta) { + $propMeta->accept($visitor, $propMeta->getPropertyValue($containingValue), $group, $property); + } + } + + return $visitor->getViolations(); + } + + /** + * {@inheritDoc} + * + * @throws ValidatorException If the metadata for the value does not support properties. + */ + public function validatePropertyValue($containingValue, $property, $value, $groups = null) + { + $visitor = $this->createVisitor($containingValue); + $metadata = $this->metadataFactory->getMetadataFor($containingValue); + + if (!$metadata instanceof PropertyMetadataContainerInterface) { + $valueAsString = is_scalar($containingValue) + ? '"' . $containingValue . '"' + : 'the value of type ' . gettype($containingValue); + + throw new ValidatorException(sprintf('The metadata for ' . $valueAsString . ' does not support properties.')); + } + + foreach ($this->resolveGroups($groups) as $group) { + foreach ($metadata->getPropertyMetadata($property) as $propMeta) { + $propMeta->accept($visitor, $value, $group, $property); + } + } + + return $visitor->getViolations(); + } + + /** + * {@inheritDoc} + */ + public function validateValue($value, $constraints, $groups = null) + { + $context = new ExecutionContext($this->createVisitor(null)); + + $constraints = is_array($constraints) ? $constraints : array($constraints); + + foreach ($constraints as $constraint) { + if ($constraint instanceof Valid) { + // Why can't the Valid constraint be executed directly? + // + // It cannot be executed like regular other constraints, because regular + // constraints are only executed *if they belong to the validated group*. + // The Valid constraint, on the other hand, is always executed and propagates + // the group to the cascaded object. The propagated group depends on + // + // * Whether a group sequence is currently being executed. Then the default + // group is propagated. + // + // * Otherwise the validated group is propagated. + + throw new ValidatorException( + sprintf( + 'The constraint %s cannot be validated. Use the method validate() instead.', + get_class($constraint) + ) + ); + } + + $context->validateValue($value, $constraint, $groups); + } + + return $context->getViolations(); + } + + /** + * @param mixed $root + * + * @return ValidationVisitor + */ + private function createVisitor($root) + { + return new ValidationVisitor($root, $this->metadataFactory, $this->validatorFactory, $this->objectInitializers); + } + + /** + * @param null|string|string[] $groups + * + * @return string[] + */ + private function resolveGroups($groups) + { + return $groups ? (array) $groups : array(Constraint::DEFAULT_GROUP); } } diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 7c6f95bbef..8f39d4ae4d 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; +use Symfony\Component\Validator\Mapping\ClassMetadataFactoryAdapter; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; @@ -60,7 +61,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface private $annotationReader = null; /** - * @var ClassMetadataFactoryInterface + * @var MetadataFactoryInterface */ private $metadataFactory; @@ -213,12 +214,17 @@ class ValidatorBuilder implements ValidatorBuilderInterface /** * {@inheritdoc} */ - public function setMetadataFactory(ClassMetadataFactoryInterface $metadataFactory) + public function setMetadataFactory($metadataFactory) { if (count($this->xmlMappings) > 0 || count($this->yamlMappings) > 0 || count($this->methodMappings) > 0 || null !== $this->annotationReader) { throw new ValidatorException('You cannot set a custom metadata factory after adding custom mappings. You should do either of both.'); } + if ($metadataFactory instanceof ClassMetadataFactoryInterface + && !$metadataFactory instanceof MetadataFactoryInterface) { + $metadataFactory = new ClassMetadataFactoryAdapter($metadataFactory); + } + $this->metadataFactory = $metadataFactory; return $this; diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index 447a16b341..914d4f96d7 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator; -use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; use Doctrine\Common\Annotations\Reader; @@ -113,11 +112,14 @@ interface ValidatorBuilderInterface /** * Sets the class metadata factory used by the validator. * - * @param ClassMetadataFactoryInterface $metadataFactory The metadata factory. + * As of Symfony 2.3, the first parameter of this method will be typed + * against {@link MetadataFactoryInterface}. + * + * @param MetadataFactoryInterface|Mapping\ClassMetadataFactoryInterface $metadataFactory The metadata factory. * * @return ValidatorBuilderInterface The builder object. */ - public function setMetadataFactory(ClassMetadataFactoryInterface $metadataFactory); + public function setMetadataFactory($metadataFactory); /** * Sets the cache for caching class metadata. diff --git a/src/Symfony/Component/Validator/ValidatorContext.php b/src/Symfony/Component/Validator/ValidatorContext.php index b18bb97a89..c2f6880a35 100644 --- a/src/Symfony/Component/Validator/ValidatorContext.php +++ b/src/Symfony/Component/Validator/ValidatorContext.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; +use Symfony\Component\Validator\Mapping\ClassMetadataFactoryAdapter; /** * Default implementation of ValidatorContextInterface @@ -23,6 +24,11 @@ use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; */ class ValidatorContext implements ValidatorContextInterface { + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + /** * The class metadata factory used in the new validator * @var ClassMetadataFactoryInterface @@ -43,6 +49,12 @@ class ValidatorContext implements ValidatorContextInterface */ public function setClassMetadataFactory(ClassMetadataFactoryInterface $classMetadataFactory) { + if ($classMetadataFactory instanceof MetadataFactoryInterface) { + $this->metadataFactory = $classMetadataFactory; + } else { + $this->metadataFactory = new ClassMetadataFactoryAdapter($classMetadataFactory); + } + $this->classMetadataFactory = $classMetadataFactory; return $this; @@ -70,7 +82,7 @@ class ValidatorContext implements ValidatorContextInterface public function getValidator() { return new Validator( - $this->classMetadataFactory, + $this->metadataFactory, $this->constraintValidatorFactory ); } diff --git a/src/Symfony/Component/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/ValidatorInterface.php index 5e2e33ebed..b56c5801f2 100644 --- a/src/Symfony/Component/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/ValidatorInterface.php @@ -14,7 +14,7 @@ namespace Symfony\Component\Validator; use Symfony\Component\Validator\Constraint; /** - * Validates a given value. + * Validates values and graphs of objects and arrays. * * @author Bernhard Schussek * @@ -23,61 +23,77 @@ use Symfony\Component\Validator\Constraint; interface ValidatorInterface { /** - * Validate the given object. + * Validates a value. * - * @param object $object The object to validate - * @param array|null $groups The validator groups to use for validating + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. * - * @return ConstraintViolationList + * @param mixed $value The value to validate + * @param array|null $groups The validation groups to validate. + * @param Boolean $traverse Whether to traverse the value if it is traversable. + * @param Boolean $deep Whether to traverse nested traversable values recursively. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. * * @api */ - public function validate($object, $groups = null); + public function validate($value, $groups = null, $traverse = false, $deep = false); /** - * Validate a single property of an object against its current value. + * Validates a property of a value against its current value. * - * @param object $object The object to validate - * @param string $property The name of the property to validate - * @param array|null $groups The validator groups to use for validating + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. * - * @return ConstraintViolationList + * @param mixed $containingValue The value containing the property. + * @param string $property The name of the property to validate. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. * * @api */ - public function validateProperty($object, $property, $groups = null); + public function validateProperty($containingValue, $property, $groups = null); /** - * Validate a single property of an object against the given value. + * Validate a property of a value against a potential value. * - * @param string $class The class on which the property belongs - * @param string $property The name of the property to validate - * @param string $value - * @param array|null $groups The validator groups to use for validating + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. * - * @return ConstraintViolationList + * @param string $containingValue The value containing the property. + * @param string $property The name of the property to validate + * @param string $value The value to validate against the + * constraints of the property. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. * * @api */ - public function validatePropertyValue($class, $property, $value, $groups = null); + public function validatePropertyValue($containingValue, $property, $value, $groups = null); /** - * Validates a given value against a specific Constraint. + * Validates a value against a constraint or a list of constraints. * - * @param mixed $value The value to validate - * @param Constraint $constraint The constraint to validate against - * @param array|null $groups The validator groups to use for validating + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param array|null $groups The validation groups to validate. * - * @return ConstraintViolationList + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. * * @api */ - public function validateValue($value, Constraint $constraint, $groups = null); + public function validateValue($value, $constraints, $groups = null); /** - * Returns the factory for ClassMetadata instances + * Returns the factory for metadata instances. * - * @return Mapping\ClassMetadataFactoryInterface + * @return MetadataFactoryInterface The metadata factory. * * @api */