[Validator] Renamed Condition to Expression and added possibility to set it onto properties

This commit is contained in:
Bernhard Schussek 2013-09-10 17:14:04 +02:00 committed by Fabien Potencier
parent a3b3a78237
commit d4ebbfd02d
13 changed files with 405 additions and 65 deletions

View File

@ -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')) {

View File

@ -10,7 +10,6 @@
<parameter key="form.factory.class">Symfony\Component\Form\FormFactory</parameter>
<parameter key="form.extension.class">Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension</parameter>
<parameter key="form.type_guesser.validator.class">Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser</parameter>
<parameter key="property_accessor.class">Symfony\Component\PropertyAccess\PropertyAccessor</parameter>
</parameters>
<services>
@ -54,9 +53,6 @@
<argument type="service" id="validator.mapping.class_metadata_factory" />
</service>
<!-- PropertyAccessor -->
<service id="property_accessor" class="%property_accessor.class%" />
<!-- CoreExtension -->
<service id="form.type.form" class="Symfony\Component\Form\Extension\Core\Type\FormType">
<argument type="service" id="property_accessor"/>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="property_accessor.class">Symfony\Component\PropertyAccess\PropertyAccessor</parameter>
</parameters>
<services>
<service id="property_accessor" class="%property_accessor.class%" />
</services>
</container>

View File

@ -17,6 +17,7 @@
<parameter key="validator.validator_factory.class">Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory</parameter>
<parameter key="validator.mapping.loader.xml_files_loader.mapping_files" type="collection" />
<parameter key="validator.mapping.loader.yaml_files_loader.mapping_files" type="collection" />
<parameter key="validator.expression.class">Symfony\Component\Validator\Constraints\ExpressionValidator</parameter>
</parameters>
<services>
@ -63,5 +64,10 @@
<service id="validator.mapping.loader.yaml_files_loader" class="%validator.mapping.loader.yaml_files_loader.class%" public="false">
<argument>%validator.mapping.loader.yaml_files_loader.mapping_files%</argument>
</service>
<service id="validator.expression" class="%validator.expression.class%">
<argument type="service" id="property_accessor" />
<tag name="validator.constraint_validator" alias="validator.expression" />
</service>
</services>
</container>

View File

@ -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 <bschussek@gmail.com>
*/
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];

View File

@ -1,50 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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;
}
}

View File

@ -17,18 +17,19 @@ use Symfony\Component\Validator\Constraint;
* @Annotation
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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';
}
}

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@symfony.com>
*/
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;
}
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,197 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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);
}
}

View File

@ -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);

View File

@ -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.
*

View File

@ -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",