diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index f0460c1a85..3b2f85aa2d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -48,6 +48,9 @@ class FrameworkExtension extends Extension
// will be used and everything will still work as expected.
$loader->load('translation.xml');
+ // Property access is used by both the Form and the Validator component
+ $loader->load('property_access.xml');
+
$loader->load('debug_prod.xml');
if ($container->getParameter('kernel.debug')) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
index 569100fce7..93fa523587 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
@@ -10,7 +10,6 @@
Symfony\Component\Form\FormFactory
Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension
Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser
- Symfony\Component\PropertyAccess\PropertyAccessor
@@ -54,9 +53,6 @@
-
-
-
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml
new file mode 100644
index 0000000000..18026144e4
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Symfony\Component\PropertyAccess\PropertyAccessor
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml
index 0bc70040a2..5f8fba2b2d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml
@@ -17,6 +17,7 @@
Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory
+ Symfony\Component\Validator\Constraints\ExpressionValidator
@@ -63,5 +64,10 @@
%validator.mapping.loader.yaml_files_loader.mapping_files%
+
+
+
+
+
diff --git a/src/Symfony/Component/Validator/ConstraintValidatorFactory.php b/src/Symfony/Component/Validator/ConstraintValidatorFactory.php
index 88b5cefdc2..88868e5336 100644
--- a/src/Symfony/Component/Validator/ConstraintValidatorFactory.php
+++ b/src/Symfony/Component/Validator/ConstraintValidatorFactory.php
@@ -11,8 +11,9 @@
namespace Symfony\Component\Validator;
-use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
-use Symfony\Component\Validator\Constraint;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+use Symfony\Component\Validator\Constraints\ExpressionValidator;
/**
* Default implementation of the ConstraintValidatorFactoryInterface.
@@ -20,11 +21,23 @@ use Symfony\Component\Validator\Constraint;
* This enforces the convention that the validatedBy() method on any
* Constrain will return the class name of the ConstraintValidator that
* should validate the Constraint.
+ *
+ * @author Bernhard Schussek
*/
class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface
{
protected $validators = array();
+ /**
+ * @var PropertyAccessorInterface
+ */
+ private $propertyAccessor;
+
+ public function __construct(PropertyAccessorInterface $propertyAccessor = null)
+ {
+ $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
+ }
+
/**
* {@inheritDoc}
*/
@@ -32,8 +45,19 @@ class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface
{
$className = $constraint->validatedBy();
- if (!isset($this->validators[$className]) || $className === 'Symfony\Component\Validator\Constraints\CollectionValidator') {
- $this->validators[$className] = new $className();
+ // The second condition is a hack that is needed when CollectionValidator
+ // calls itself recursively (Collection constraints can be nested).
+ // Since the context of the validator is overwritten when initialize()
+ // is called for the nested constraint, the outer validator is
+ // acting on the wrong context when the nested validation terminates.
+ //
+ // A better solution - which should be approached in Symfony 3.0 - is to
+ // remove the initialize() method and pass the context as last argument
+ // to validate() instead.
+ if (!isset($this->validators[$className]) || 'Symfony\Component\Validator\Constraints\CollectionValidator' === $className) {
+ $this->validators[$className] = 'validator.expression' === $className
+ ? new ExpressionValidator($this->propertyAccessor)
+ : new $className();
}
return $this->validators[$className];
diff --git a/src/Symfony/Component/Validator/Constraints/ConditionValidator.php b/src/Symfony/Component/Validator/Constraints/ConditionValidator.php
deleted file mode 100644
index 6f7f572a5c..0000000000
--- a/src/Symfony/Component/Validator/Constraints/ConditionValidator.php
+++ /dev/null
@@ -1,50 +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\Constraints;
-
-use Symfony\Component\Validator\Constraint;
-use Symfony\Component\Validator\ConstraintValidator;
-use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
-
-/**
- * @author Fabien Potencier
- */
-class ConditionValidator extends ConstraintValidator
-{
- private $expressionLanguage;
-
- /**
- * {@inheritDoc}
- */
- public function validate($object, Constraint $constraint)
- {
- if (null === $object) {
- return;
- }
-
- if (!$this->getExpressionLanguage()->evaluate($constraint->condition, array('this' => $object))) {
- $this->context->addViolation($constraint->message);
- }
- }
-
- private function getExpressionLanguage()
- {
- if (null === $this->expressionLanguage) {
- if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
- throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
- }
- $this->expressionLanguage = new ExpressionLanguage();
- }
-
- return $this->expressionLanguage;
- }
-}
diff --git a/src/Symfony/Component/Validator/Constraints/Condition.php b/src/Symfony/Component/Validator/Constraints/Expression.php
similarity index 66%
rename from src/Symfony/Component/Validator/Constraints/Condition.php
rename to src/Symfony/Component/Validator/Constraints/Expression.php
index 88c1a96279..b845a32392 100644
--- a/src/Symfony/Component/Validator/Constraints/Condition.php
+++ b/src/Symfony/Component/Validator/Constraints/Expression.php
@@ -17,18 +17,19 @@ use Symfony\Component\Validator\Constraint;
* @Annotation
*
* @author Fabien Potencier
+ * @author Bernhard Schussek
*/
-class Condition extends Constraint
+class Expression extends Constraint
{
public $message = 'This value is not valid.';
- public $condition;
+ public $expression;
/**
* {@inheritDoc}
*/
public function getDefaultOption()
{
- return 'condition';
+ return 'expression';
}
/**
@@ -36,13 +37,22 @@ class Condition extends Constraint
*/
public function getRequiredOptions()
{
- return array('condition');
+ return array('expression');
}
+
/**
* {@inheritDoc}
*/
public function getTargets()
{
- return self::CLASS_CONSTRAINT;
+ return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function validatedBy()
+ {
+ return 'validator.expression';
}
}
diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php
new file mode 100644
index 0000000000..e27859b08c
--- /dev/null
+++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Constraints;
+
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+use Symfony\Component\Validator\Exception\RuntimeException;
+
+/**
+ * @author Fabien Potencier
+ * @author Bernhard Schussek
+ */
+class ExpressionValidator extends ConstraintValidator
+{
+ /**
+ * @var PropertyAccessorInterface
+ */
+ private $propertyAccessor;
+
+ /**
+ * @var ExpressionLanguage
+ */
+ private $expressionLanguage;
+
+ public function __construct(PropertyAccessorInterface $propertyAccessor)
+ {
+ $this->propertyAccessor = $propertyAccessor;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function validate($value, Constraint $constraint)
+ {
+ if (null === $value || '' === $value) {
+ return;
+ }
+
+ $variables = array();
+
+ if (null === $this->context->getPropertyName()) {
+ $variables['this'] = $value;
+ } else {
+ // Extract the object that the property belongs to from the object
+ // graph
+ $path = new PropertyPath($this->context->getPropertyPath());
+ $parentPath = $path->getParent();
+ $root = $this->context->getRoot();
+
+ $variables['value'] = $value;
+ $variables['this'] = $parentPath ? $this->propertyAccessor->getValue($root, $parentPath) : $root;
+ }
+
+ if (!$this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) {
+ $this->context->addViolation($constraint->message);
+ }
+ }
+
+ private function getExpressionLanguage()
+ {
+ if (null === $this->expressionLanguage) {
+ if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
+ throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
+ }
+ $this->expressionLanguage = new ExpressionLanguage();
+ }
+
+ return $this->expressionLanguage;
+ }
+}
diff --git a/src/Symfony/Component/Validator/Exception/RuntimeException.php b/src/Symfony/Component/Validator/Exception/RuntimeException.php
new file mode 100644
index 0000000000..df4a50c474
--- /dev/null
+++ b/src/Symfony/Component/Validator/Exception/RuntimeException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Exception;
+
+/**
+ * Base RuntimeException for the Validator component.
+ *
+ * @author Bernhard Schussek
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php
new file mode 100644
index 0000000000..b71138e5f6
--- /dev/null
+++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php
@@ -0,0 +1,197 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator\Tests\Constraints;
+
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\Validator\Constraints\Expression;
+use Symfony\Component\Validator\Constraints\ExpressionValidator;
+
+class ExpressionValidatorTest extends \PHPUnit_Framework_TestCase
+{
+ protected $context;
+ protected $validator;
+
+ protected function setUp()
+ {
+ $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false);
+ $this->validator = new ExpressionValidator(PropertyAccess::createPropertyAccessor());
+ $this->validator->initialize($this->context);
+
+ $this->context->expects($this->any())
+ ->method('getClassName')
+ ->will($this->returnValue(__CLASS__));
+ }
+
+ protected function tearDown()
+ {
+ $this->context = null;
+ $this->validator = null;
+ }
+
+ public function testNullIsValid()
+ {
+ $this->context->expects($this->never())
+ ->method('addViolation');
+
+ $this->validator->validate(null, new Expression('value == 1'));
+ }
+
+ public function testEmptyStringIsValid()
+ {
+ $this->context->expects($this->never())
+ ->method('addViolation');
+
+ $this->validator->validate('', new Expression('value == 1'));
+ }
+
+ public function testSucceedingExpressionAtObjectLevel()
+ {
+ $constraint = new Expression('this.property == 1');
+
+ $object = (object) array('property' => '1');
+
+ $this->context->expects($this->any())
+ ->method('getPropertyName')
+ ->will($this->returnValue(null));
+
+ $this->context->expects($this->never())
+ ->method('addViolation');
+
+ $this->validator->validate($object, $constraint);
+ }
+
+ public function testFailingExpressionAtObjectLevel()
+ {
+ $constraint = new Expression(array(
+ 'expression' => 'this.property == 1',
+ 'message' => 'myMessage',
+ ));
+
+ $object = (object) array('property' => '2');
+
+ $this->context->expects($this->any())
+ ->method('getPropertyName')
+ ->will($this->returnValue(null));
+
+ $this->context->expects($this->once())
+ ->method('addViolation')
+ ->with('myMessage');
+
+ $this->validator->validate($object, $constraint);
+ }
+
+ public function testSucceedingExpressionAtPropertyLevel()
+ {
+ $constraint = new Expression('value == this.expected');
+
+ $object = (object) array('expected' => '1');
+
+ $this->context->expects($this->any())
+ ->method('getPropertyName')
+ ->will($this->returnValue('property'));
+
+ $this->context->expects($this->any())
+ ->method('getPropertyPath')
+ ->will($this->returnValue('property'));
+
+ $this->context->expects($this->any())
+ ->method('getRoot')
+ ->will($this->returnValue($object));
+
+ $this->context->expects($this->never())
+ ->method('addViolation');
+
+ $this->validator->validate('1', $constraint);
+ }
+
+ public function testFailingExpressionAtPropertyLevel()
+ {
+ $constraint = new Expression(array(
+ 'expression' => 'value == this.expected',
+ 'message' => 'myMessage',
+ ));
+
+ $object = (object) array('expected' => '1');
+
+ $this->context->expects($this->any())
+ ->method('getPropertyName')
+ ->will($this->returnValue('property'));
+
+ $this->context->expects($this->any())
+ ->method('getPropertyPath')
+ ->will($this->returnValue('property'));
+
+ $this->context->expects($this->any())
+ ->method('getRoot')
+ ->will($this->returnValue($object));
+
+ $this->context->expects($this->once())
+ ->method('addViolation')
+ ->with('myMessage');
+
+ $this->validator->validate('2', $constraint);
+ }
+
+ public function testSucceedingExpressionAtNestedPropertyLevel()
+ {
+ $constraint = new Expression('value == this.expected');
+
+ $object = (object) array('expected' => '1');
+ $root = (object) array('nested' => $object);
+
+ $this->context->expects($this->any())
+ ->method('getPropertyName')
+ ->will($this->returnValue('property'));
+
+ $this->context->expects($this->any())
+ ->method('getPropertyPath')
+ ->will($this->returnValue('nested.property'));
+
+ $this->context->expects($this->any())
+ ->method('getRoot')
+ ->will($this->returnValue($root));
+
+ $this->context->expects($this->never())
+ ->method('addViolation');
+
+ $this->validator->validate('1', $constraint);
+ }
+
+ public function testFailingExpressionAtNestedPropertyLevel()
+ {
+ $constraint = new Expression(array(
+ 'expression' => 'value == this.expected',
+ 'message' => 'myMessage',
+ ));
+
+ $object = (object) array('expected' => '1');
+ $root = (object) array('nested' => $object);
+
+ $this->context->expects($this->any())
+ ->method('getPropertyName')
+ ->will($this->returnValue('property'));
+
+ $this->context->expects($this->any())
+ ->method('getPropertyPath')
+ ->will($this->returnValue('nested.property'));
+
+ $this->context->expects($this->any())
+ ->method('getRoot')
+ ->will($this->returnValue($root));
+
+ $this->context->expects($this->once())
+ ->method('addViolation')
+ ->with('myMessage');
+
+ $this->validator->validate('2', $constraint);
+ }
+}
diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php
index a5bfc1fdf6..159070be41 100644
--- a/src/Symfony/Component/Validator/ValidatorBuilder.php
+++ b/src/Symfony/Component/Validator/ValidatorBuilder.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Validator;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Mapping\ClassMetadataFactory;
use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
@@ -84,6 +86,11 @@ class ValidatorBuilder implements ValidatorBuilderInterface
*/
private $translationDomain;
+ /**
+ * @var PropertyAccessorInterface
+ */
+ private $propertyAccessor;
+
/**
* {@inheritdoc}
*/
@@ -253,6 +260,10 @@ class ValidatorBuilder implements ValidatorBuilderInterface
*/
public function setConstraintValidatorFactory(ConstraintValidatorFactoryInterface $validatorFactory)
{
+ if (null !== $this->propertyAccessor) {
+ throw new ValidatorException('You cannot set a validator factory after setting a custom property accessor. Remove the call to setPropertyAccessor() if you want to call setConstraintValidatorFactory().');
+ }
+
$this->validatorFactory = $validatorFactory;
return $this;
@@ -278,6 +289,20 @@ class ValidatorBuilder implements ValidatorBuilderInterface
return $this;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor)
+ {
+ if (null !== $this->validatorFactory) {
+ throw new ValidatorException('You cannot set a property accessor after setting a custom validator factory. Configure your validator factory instead.');
+ }
+
+ $this->propertyAccessor = $propertyAccessor;
+
+ return $this;
+ }
+
/**
* {@inheritdoc}
*/
@@ -319,7 +344,8 @@ class ValidatorBuilder implements ValidatorBuilderInterface
$metadataFactory = new ClassMetadataFactory($loader, $this->metadataCache);
}
- $validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory();
+ $propertyAccessor = $this->propertyAccessor ?: PropertyAccess::createPropertyAccessor();
+ $validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory($propertyAccessor);
$translator = $this->translator ?: new DefaultTranslator();
return new Validator($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers);
diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php
index 99f367b6aa..92aaca756a 100644
--- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php
+++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Validator;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Mapping\Cache\CacheInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Doctrine\Common\Annotations\Reader;
@@ -159,6 +160,15 @@ interface ValidatorBuilderInterface
*/
public function setTranslationDomain($translationDomain);
+ /**
+ * Sets the property accessor for resolving property paths.
+ *
+ * @param PropertyAccessorInterface $propertyAccessor The property accessor.
+ *
+ * @return ValidatorBuilderInterface The builder object.
+ */
+ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor);
+
/**
* Builds and returns a new validator object.
*
diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json
index de06e2326d..d6f6195825 100644
--- a/src/Symfony/Component/Validator/composer.json
+++ b/src/Symfony/Component/Validator/composer.json
@@ -17,7 +17,8 @@
],
"require": {
"php": ">=5.3.3",
- "symfony/translation": "~2.0"
+ "symfony/translation": "~2.0",
+ "symfony/property-access": "~2.2"
},
"require-dev": {
"symfony/http-foundation": "~2.1",