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 */