From c6f3dd6a85eb5893df94ab2c9646842b53e47f29 Mon Sep 17 00:00:00 2001 From: Felds Liscia Date: Fri, 24 May 2013 14:02:19 -0300 Subject: [PATCH 001/323] add allow_extra_fields option --- .../Validator/Constraints/FormValidator.php | 2 +- .../Type/FormTypeValidatorExtension.php | 1 + .../Constraints/FormValidatorTest.php | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 1b8b68ed838cd..b14523755f913 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -107,7 +107,7 @@ public function validate($form, Constraint $constraint) } // Mark the form with an error if it contains extra fields - if (count($form->getExtraData()) > 0) { + if (!$config->getOption('allow_extra_fields') && count($form->getExtraData()) > 0) { $this->context->addViolation( $config->getOption('extra_fields_message'), array('{{ extra_fields }}' => implode('", "', array_keys($form->getExtraData()))), diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php index 99ef51f2bfed5..38e1ca5d31d99 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -67,6 +67,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'cascade_validation' => false, 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), + 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', )); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php index 79d004cd6f294..2c4a239ae8164 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -615,6 +615,28 @@ public function testViolationIfExtraData() $this->validator->validate($form, new Form()); } + public function testNoViolationIfAllowExtraData() + { + $context = $this->getMockExecutionContext(); + + $form = $this + ->getBuilder('parent', null, array('allow_extra_fields' => true)) + ->setCompound(true) + ->setDataMapper($this->getDataMapper()) + ->add($this->getBuilder('child')) + ->getForm(); + + $form->bind(array('foo' => 'bar')); + + $context->expects($this->never()) + ->method('addViolation'); + $context->expects($this->never()) + ->method('addViolationAt'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + /** * @dataProvider getPostMaxSizeFixtures */ From 7bc7a8a6ec83cd9f1b7efd5a1889a28763063eb6 Mon Sep 17 00:00:00 2001 From: Matt Janssen Date: Mon, 30 Sep 2013 22:14:37 -0500 Subject: [PATCH 002/323] [Form] Accept interfaces to be passed to "data_class" --- src/Symfony/Component/Form/FormConfigBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index 1015da4f51829..391c5cd5e2586 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -192,7 +192,7 @@ public function __construct($name, $dataClass, EventDispatcherInterface $dispatc { self::validateName($name); - if (null !== $dataClass && !class_exists($dataClass)) { + if (null !== $dataClass && !class_exists($dataClass) && !interface_exists($dataClass)) { throw new InvalidArgumentException(sprintf('The data class "%s" is not a valid class.', $dataClass)); } From b44e07b7f6badf646a3a6758b5817eea8234d915 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 28 Mar 2014 12:44:24 +0100 Subject: [PATCH 003/323] [Form] Added test case for 4759e062ed004749dbdc2ba31aef0f8ac2601895 --- src/Symfony/Component/Form/CHANGELOG.md | 1 + .../Extension/Core/Type/FormTypeTest.php | 31 +++++++++++++++++++ .../Form/Tests/Fixtures/AbstractAuthor.php | 16 ++++++++++ .../Form/Tests/Fixtures/AuthorInterface.php | 16 ++++++++++ 4 files changed, 64 insertions(+) create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/AbstractAuthor.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/AuthorInterface.php diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index c210f9e7924ac..dc7a59bbde922 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * [BC BREAK] added two optional parameters to FormInterface::getErrors() and changed the method to return a Symfony\Component\Form\FormErrorIterator instance instead of an array + * you can now pass interface names in the "data_class" option 2.4.0 ----- diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php index 6a6a17d84ae3b..a8465dc99083a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php @@ -146,6 +146,37 @@ public function testPassMaxLengthBCToView() $this->assertSame(10, $view->vars['attr']['maxlength']); } + public function testDataClassMayBeNull() + { + $this->factory->createBuilder('form', null, array( + 'data_class' => null, + )); + } + + public function testDataClassMayBeAbstractClass() + { + $this->factory->createBuilder('form', null, array( + 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\AbstractAuthor', + )); + } + + public function testDataClassMayBeInterface() + { + $this->factory->createBuilder('form', null, array( + 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\AuthorInterface', + )); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException + */ + public function testDataClassMustBeValidClassOrInterface() + { + $this->factory->createBuilder('form', null, array( + 'data_class' => 'foobar', + )); + } + public function testSubmitWithEmptyDataCreatesObjectIfClassAvailable() { $builder = $this->factory->createBuilder('form', null, array( diff --git a/src/Symfony/Component/Form/Tests/Fixtures/AbstractAuthor.php b/src/Symfony/Component/Form/Tests/Fixtures/AbstractAuthor.php new file mode 100644 index 0000000000000..03a6b724f308e --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/AbstractAuthor.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +abstract class AbstractAuthor +{ +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/AuthorInterface.php b/src/Symfony/Component/Form/Tests/Fixtures/AuthorInterface.php new file mode 100644 index 0000000000000..984cb541ec8e7 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/AuthorInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +interface AuthorInterface +{ +} From 0488389d95033a60b3de14340c5a532c6a6af18f Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 28 Mar 2014 15:50:34 +0100 Subject: [PATCH 004/323] [PropertyAccess] Refactored PropertyAccessorTest --- .../PropertyAccess/PropertyAccessor.php | 11 +- .../PropertyAccess/Tests/Fixtures/Author.php | 71 --- .../Tests/Fixtures/Magician.php | 27 -- .../Tests/Fixtures/MagicianCall.php | 28 -- .../Tests/Fixtures/TestClass.php | 115 +++++ .../Tests/Fixtures/TestClassMagicCall.php | 39 ++ .../Tests/Fixtures/TestClassMagicGet.php | 42 ++ .../Tests/PropertyAccessorTest.php | 411 ++++++------------ 8 files changed, 336 insertions(+), 408 deletions(-) delete mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php delete mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php delete mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index d48891ef275d8..f5183a109e574 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -116,7 +116,7 @@ public function setValue(&$objectOrArray, $propertyPath, $value) * * @throws UnexpectedTypeException If a value within the path is neither object nor array. */ - private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $throwExceptionOnNonexistantIndex = false) + private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $throwExceptionOnInvalidIndex = false) { $propertyValues = array(); @@ -131,9 +131,10 @@ private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $pr // Create missing nested arrays on demand if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { - if ($throwExceptionOnNonexistantIndex) { + if ($throwExceptionOnInvalidIndex) { throw new NoSuchIndexException(sprintf('Cannot read property "%s". Available properties are "%s"', $property, print_r(array_keys($objectOrArray), true))); } + $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null; } @@ -412,8 +413,8 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula $addMethod = 'add'.$singular; $removeMethod = 'remove'.$singular; - $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1); - $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1); + $addMethodFound = $this->isMethodAccessible($reflClass, $addMethod, 1); + $removeMethodFound = $this->isMethodAccessible($reflClass, $removeMethod, 1); if ($addMethodFound && $removeMethodFound) { return array($addMethod, $removeMethod); @@ -442,7 +443,7 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula * @return Boolean Whether the method is public and has $parameters * required parameters */ - private function isAccessible(\ReflectionClass $class, $methodName, $parameters) + private function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters) { if ($class->hasMethod($methodName)) { $method = $class->getMethod($methodName); diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php deleted file mode 100644 index ed2331bab04fb..0000000000000 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\PropertyAccess\Tests\Fixtures; - -class Author -{ - public $firstName; - private $lastName; - private $australian; - public $child; - private $readPermissions; - - private $privateProperty; - - public function setLastName($lastName) - { - $this->lastName = $lastName; - } - - public function getLastName() - { - return $this->lastName; - } - - private function getPrivateGetter() - { - return 'foobar'; - } - - public function setAustralian($australian) - { - $this->australian = $australian; - } - - public function isAustralian() - { - return $this->australian; - } - - public function setReadPermissions($bool) - { - $this->readPermissions = $bool; - } - - public function hasReadPermissions() - { - return $this->readPermissions; - } - - private function isPrivateIsser() - { - return true; - } - - public function getPrivateSetter() - { - } - - private function setPrivateSetter($data) - { - } -} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php deleted file mode 100644 index 6faa5dbf7bb3c..0000000000000 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\PropertyAccess\Tests\Fixtures; - -class Magician -{ - private $foobar; - - public function __set($property, $value) - { - $this->$property = $value; - } - - public function __get($property) - { - return isset($this->$property) ? $this->$property : null; - } -} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php deleted file mode 100644 index 0508a71da1e43..0000000000000 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/MagicianCall.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\PropertyAccess\Tests\Fixtures; - -class MagicianCall -{ - private $foobar; - - public function __call($name, $args) - { - $property = lcfirst(substr($name, 3)); - if ('get' === substr($name, 0, 3)) { - return isset($this->$property) ? $this->$property : null; - } elseif ('set' === substr($name, 0, 3)) { - $value = 1 == count($args) ? $args[0] : null; - $this->$property = $value; - } - } -} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php new file mode 100644 index 0000000000000..178d7f618abb5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClass +{ + public $publicProperty; + protected $protectedProperty; + private $privateProperty; + + private $publicAccessor; + private $publicIsAccessor; + private $publicHasAccessor; + + public function __construct($value) + { + $this->publicProperty = $value; + $this->publicAccessor = $value; + $this->publicIsAccessor = $value; + $this->publicHasAccessor = $value; + } + + public function setPublicAccessor($value) + { + $this->publicAccessor = $value; + } + + public function getPublicAccessor() + { + return $this->publicAccessor; + } + + public function setPublicIsAccessor($value) + { + $this->publicIsAccessor = $value; + } + + public function isPublicIsAccessor() + { + return $this->publicIsAccessor; + } + + public function setPublicHasAccessor($value) + { + $this->publicHasAccessor = $value; + } + + public function hasPublicHasAccessor() + { + return $this->publicHasAccessor; + } + + protected function setProtectedAccessor($value) + { + } + + protected function getProtectedAccessor() + { + return 'foobar'; + } + + protected function setProtectedIsAccessor($value) + { + } + + protected function isProtectedIsAccessor() + { + return 'foobar'; + } + + protected function setProtectedHasAccessor($value) + { + } + + protected function hasProtectedHasAccessor() + { + return 'foobar'; + } + + private function setPrivateAccessor($value) + { + } + + private function getPrivateAccessor() + { + return 'foobar'; + } + + private function setPrivateIsAccessor($value) + { + } + + private function isPrivateIsAccessor() + { + return 'foobar'; + } + + private function setPrivateHasAccessor($value) + { + } + + private function hasPrivateHasAccessor() + { + return 'foobar'; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php new file mode 100644 index 0000000000000..d49967abd1a66 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassMagicCall +{ + private $magicCallProperty; + + public function __construct($value) + { + $this->magicCallProperty = $value; + } + + public function __call($method, array $args) + { + if ('getMagicCallProperty' === $method) { + return $this->magicCallProperty; + } + + if ('getConstantMagicCallProperty' === $method) { + return 'constant value'; + } + + if ('setMagicCallProperty' === $method) { + $this->magicCallProperty = reset($args); + } + + return null; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php new file mode 100644 index 0000000000000..fee8318966728 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassMagicGet +{ + private $magicProperty; + + public function __construct($value) + { + $this->magicProperty = $value; + } + + public function __set($property, $value) + { + if ('magicProperty' === $property) { + $this->magicProperty = $value; + } + } + + public function __get($property) + { + if ('magicProperty' === $property) { + return $this->magicProperty; + } + + if ('constantMagicProperty' === $property) { + return 'constant value'; + } + + return null; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 72fbfe428f4fc..3c13e50bb2101 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -12,228 +12,157 @@ namespace Symfony\Component\PropertyAccess\Tests; use Symfony\Component\PropertyAccess\PropertyAccessor; -use Symfony\Component\PropertyAccess\Tests\Fixtures\Author; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; -use Symfony\Component\PropertyAccess\Tests\Fixtures\MagicianCall; -use Symfony\Component\PropertyAccess\PropertyAccessorBuilder; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet; class PropertyAccessorTest extends \PHPUnit_Framework_TestCase { /** - * @var PropertyAccessorBuilder + * @var PropertyAccessor */ - private $propertyAccessorBuilder; + private $propertyAccessor; protected function setUp() { - $this->propertyAccessorBuilder = new PropertyAccessorBuilder(); + $this->propertyAccessor = new PropertyAccessor(); } - /** - * Get PropertyAccessor configured - * - * @param string $withMagicCall - * @param string $throwExceptionOnInvalidIndex - * @return PropertyAccessorInterface - */ - protected function getPropertyAccessor($withMagicCall = false, $throwExceptionOnInvalidIndex = false) - { - if ($withMagicCall) { - $this->propertyAccessorBuilder->enableMagicCall(); - } else { - $this->propertyAccessorBuilder->disableMagicCall(); - } - - if ($throwExceptionOnInvalidIndex) { - $this->propertyAccessorBuilder->enableExceptionOnInvalidIndex(); - } else { - $this->propertyAccessorBuilder->disableExceptionOnInvalidIndex(); - } - - return $this->propertyAccessorBuilder->getPropertyAccessor(); - } - - public function testGetValueReadsArray() - { - $array = array('firstName' => 'Bernhard'); - - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[firstName]')); - } - - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - */ - public function testGetValueThrowsExceptionIfIndexNotationExpected() - { - $array = array('firstName' => 'Bernhard'); - - $this->getPropertyAccessor()->getValue($array, 'firstName'); - } - - public function testGetValueReadsZeroIndex() + public function getValidPropertyPaths() { - $array = array('Bernhard'); - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[0]')); - } - - public function testGetValueReadsIndexWithSpecialChars() - { - $array = array('%!@$§.' => 'Bernhard'); - - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[%!@$§.]')); - } + return array( + array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'), + array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'), + array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), + array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'), + array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'), + array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'), + array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'), + array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'), - public function testGetValueReadsNestedIndexWithSpecialChars() - { - $array = array('root' => array('%!@$§.' => 'Bernhard')); + // Accessor methods + array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'), - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[root][%!@$§.]')); - } + // Methods are camelized + array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'), - public function testGetValueReadsArrayWithCustomPropertyPath() - { - $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); + // Missing indices + array(array('index' => array()), '[index][firstName]', null), + array(array('root' => array('index' => array())), '[root][index][firstName]', null), - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[child][index][firstName]')); + // Special chars + array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'), + array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'), + array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'), + array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'), + ); } - public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath() + public function getPathsWithMissingProperty() { - $array = array('child' => array('index' => array())); - // No BC break - $this->assertNull($this->getPropertyAccessor()->getValue($array, '[child][index][firstName]')); + return array( + array((object) array('firstName' => 'Bernhard'), 'lastName'), + array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.lastName'), + array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].lastName'), + array(new TestClass('Bernhard'), 'protectedProperty'), + array(new TestClass('Bernhard'), 'privateProperty'), + array(new TestClass('Bernhard'), 'protectedAccessor'), + array(new TestClass('Bernhard'), 'protectedIsAccessor'), + array(new TestClass('Bernhard'), 'protectedHasAccessor'), + array(new TestClass('Bernhard'), 'privateAccessor'), + array(new TestClass('Bernhard'), 'privateIsAccessor'), + array(new TestClass('Bernhard'), 'privateHasAccessor'), - try { - $this->getPropertyAccessor(false, true)->getValue($array, '[child][index][firstName]'); - $this->fail('Getting value on a nonexistent path from array should throw a Symfony\Component\PropertyAccess\Exception\NoSuchIndexException exception'); - } catch (\Exception $e) { - $this->assertInstanceof('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException', $e, 'Getting value on a nonexistent path from array should throw a Symfony\Component\PropertyAccess\Exception\NoSuchIndexException exception'); - } + // Properties are not camelized + array(new TestClass('Bernhard'), 'public_property'), + ); } - public function testGetValueReadsProperty() + public function getPathsWithMissingIndex() { - $object = new Author(); - $object->firstName = 'Bernhard'; - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($object, 'firstName')); - } - - public function testGetValueIgnoresSingular() - { - $this->markTestSkipped('This feature is temporarily disabled as of 2.1'); - - $object = (object) array('children' => 'Many'); - - $this->assertEquals('Many', $this->getPropertyAccessor()->getValue($object, 'children|child')); - } - - public function testGetValueReadsPropertyWithSpecialCharsExceptDot() - { - $array = (object) array('%!@$§' => 'Bernhard'); - - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '%!@$§')); - } - - public function testGetValueReadsPropertyWithSpecialCharsExceptDotNested() - { - $object = (object) array('nested' => (object) array('@child' => 'foo')); - - $this->assertEquals('foo', $this->getPropertyAccessor()->getValue($object, 'nested.@child')); - } - - public function testGetValueReadsPropertyWithCustomPropertyPath() - { - $object = new Author(); - $object->child = array(); - $object->child['index'] = new Author(); - $object->child['index']->firstName = 'Bernhard'; - - $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($object, 'child[index].firstName')); + return array( + array(array('firstName' => 'Bernhard'), '[lastName]'), + array(array(), '[index][lastName]'), + array(array('index' => array()), '[index][lastName]'), + array(array('index' => array('firstName' => 'Bernhard')), '[index][lastName]'), + array((object) array('property' => array('firstName' => 'Bernhard')), 'property[lastName]'), + ); } /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @dataProvider getValidPropertyPaths */ - public function testGetValueThrowsExceptionIfPropertyIsNotPublic() - { - $this->getPropertyAccessor()->getValue(new Author(), 'privateProperty'); - } - - public function testGetValueReadsGetters() + public function testGetValue($objectOrArray, $path, $value) { - $object = new Author(); - $object->setLastName('Schussek'); - - $this->assertEquals('Schussek', $this->getPropertyAccessor()->getValue($object, 'lastName')); - } - - public function testGetValueCamelizesGetterNames() - { - $object = new Author(); - $object->setLastName('Schussek'); - - $this->assertEquals('Schussek', $this->getPropertyAccessor()->getValue($object, 'last_name')); + $this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path)); } /** + * @dataProvider getPathsWithMissingProperty * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ - public function testGetValueThrowsExceptionIfGetterIsNotPublic() + public function testGetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) { - $this->getPropertyAccessor()->getValue(new Author(), 'privateGetter'); + $this->propertyAccessor->getValue($objectOrArray, $path); } - public function testGetValueReadsIssers() + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testGetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) { - $object = new Author(); - $object->setAustralian(false); - - $this->assertFalse($this->getPropertyAccessor()->getValue($object, 'australian')); + $this->assertNull($this->propertyAccessor->getValue($objectOrArray, $path)); } - public function testGetValueReadHassers() + /** + * @dataProvider getPathsWithMissingIndex + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) { - $object = new Author(); - $object->setReadPermissions(true); - - $this->assertTrue($this->getPropertyAccessor()->getValue($object, 'read_permissions')); + $this->propertyAccessor = new PropertyAccessor(false, true); + $this->propertyAccessor->getValue($objectOrArray, $path); } public function testGetValueReadsMagicGet() { - $object = new Magician(); - $object->__set('magicProperty', 'foobar'); - - $this->assertSame('foobar', $this->getPropertyAccessor()->getValue($object, 'magicProperty')); + $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty')); } - /* - * https://github.com/symfony/symfony/pull/4450 - */ + // https://github.com/symfony/symfony/pull/4450 public function testGetValueReadsMagicGetThatReturnsConstant() { - $object = new Magician(); - - $this->assertNull($this->getPropertyAccessor()->getValue($object, 'magicProperty')); + $this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'constantMagicProperty')); } /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ - public function testGetValueThrowsExceptionIfIsserIsNotPublic() + public function testGetValueDoesNotReadMagicCallByDefault() { - $this->getPropertyAccessor()->getValue(new Author(), 'privateIsser'); + $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty'); } - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - */ - public function testGetValueThrowsExceptionIfPropertyDoesNotExist() + public function testGetValueReadsMagicCallIfEnabled() { - $this->getPropertyAccessor()->getValue(new Author(), 'foobar'); + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + // https://github.com/symfony/symfony/pull/4450 + public function testGetValueReadsMagicCallThatReturnsConstant() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertSame('constant value', $this->propertyAccessor->getValue(new TestClassMagicCall('Bernhard'), 'constantMagicCallProperty')); } /** @@ -241,7 +170,7 @@ public function testGetValueThrowsExceptionIfPropertyDoesNotExist() */ public function testGetValueThrowsExceptionIfNotObjectOrArray() { - $this->getPropertyAccessor()->getValue('baz', 'foobar'); + $this->propertyAccessor->getValue('baz', 'foobar'); } /** @@ -249,7 +178,7 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray() */ public function testGetValueThrowsExceptionIfNull() { - $this->getPropertyAccessor()->getValue(null, 'foobar'); + $this->propertyAccessor->getValue(null, 'foobar'); } /** @@ -257,101 +186,77 @@ public function testGetValueThrowsExceptionIfNull() */ public function testGetValueThrowsExceptionIfEmpty() { - $this->getPropertyAccessor()->getValue('', 'foobar'); + $this->propertyAccessor->getValue('', 'foobar'); } - public function testSetValueUpdatesArrays() + /** + * @dataProvider getValidPropertyPaths + */ + public function testSetValue($objectOrArray, $path) { - $array = array(); - - $this->getPropertyAccessor()->setValue($array, '[firstName]', 'Bernhard'); + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); - $this->assertEquals(array('firstName' => 'Bernhard'), $array); + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } /** + * @dataProvider getPathsWithMissingProperty * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ - public function testSetValueThrowsExceptionIfIndexNotationExpected() + public function testSetValueThrowsExceptionIfPropertyNotFound($objectOrArray, $path) { - $array = array(); - - $this->getPropertyAccessor()->setValue($array, 'firstName', 'Bernhard'); + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); } - public function testSetValueUpdatesArraysWithCustomPropertyPath() + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testSetValueThrowsNoExceptionIfIndexNotFound($objectOrArray, $path) { - $array = array(); - - $this->getPropertyAccessor()->setValue($array, '[child][index][firstName]', 'Bernhard'); + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); - $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } - public function testSetValueUpdatesProperties() - { - $object = new Author(); - - $this->getPropertyAccessor()->setValue($object, 'firstName', 'Bernhard'); - - $this->assertEquals('Bernhard', $object->firstName); - } - - public function testSetValueUpdatesPropertiesWithCustomPropertyPath() + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) { - $object = new Author(); - $object->child = array(); - $object->child['index'] = new Author(); - - $this->getPropertyAccessor()->setValue($object, 'child[index].firstName', 'Bernhard'); + $this->propertyAccessor = new PropertyAccessor(false, true); + $this->propertyAccessor->setValue($objectOrArray, $path, 'Updated'); - $this->assertEquals('Bernhard', $object->child['index']->firstName); + $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } - public function testSetValueUpdateMagicSet() + public function testSetValueUpdatesMagicSet() { - $object = new Magician(); + $author = new TestClassMagicGet('Bernhard'); - $this->getPropertyAccessor()->setValue($object, 'magicProperty', 'foobar'); + $this->propertyAccessor->setValue($author, 'magicProperty', 'Updated'); - $this->assertEquals('foobar', $object->__get('magicProperty')); + $this->assertEquals('Updated', $author->__get('magicProperty')); } - public function testSetValueUpdatesSetters() + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueDoesNotUpdateMagicCallByDefault() { - $object = new Author(); - - $this->getPropertyAccessor()->setValue($object, 'lastName', 'Schussek'); + $author = new TestClassMagicCall('Bernhard'); - $this->assertEquals('Schussek', $object->getLastName()); + $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); } - public function testSetValueCamelizesSetterNames() + public function testSetValueUpdatesMagicCallIfEnabled() { - $object = new Author(); + $this->propertyAccessor = new PropertyAccessor(true); - $this->getPropertyAccessor()->setValue($object, 'last_name', 'Schussek'); + $author = new TestClassMagicCall('Bernhard'); - $this->assertEquals('Schussek', $object->getLastName()); - } - - public function testSetValueWithSpecialCharsNested() - { - $object = new \stdClass(); - $person = new \stdClass(); - $person->{'@email'} = null; - $object->person = $person; + $this->propertyAccessor->setValue($author, 'magicCallProperty', 'Updated'); - $this->getPropertyAccessor()->setValue($object, 'person.@email', 'bar'); - $this->assertEquals('bar', $object->person->{'@email'}); - } - - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - */ - public function testSetValueThrowsExceptionIfGetterIsNotPublic() - { - $this->getPropertyAccessor()->setValue(new Author(), 'privateSetter', 'foobar'); + $this->assertEquals('Updated', $author->__call('getMagicCallProperty', array())); } /** @@ -361,7 +266,7 @@ public function testSetValueThrowsExceptionIfNotObjectOrArray() { $value = 'baz'; - $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); } /** @@ -371,7 +276,7 @@ public function testSetValueThrowsExceptionIfNull() { $value = null; - $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); } /** @@ -381,54 +286,6 @@ public function testSetValueThrowsExceptionIfEmpty() { $value = ''; - $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); - } - - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - */ - public function testSetValueFailsIfMagicCallDisabled() - { - $value = new MagicianCall(); - - $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); - } - - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - */ - public function testGetValueFailsIfMagicCallDisabled() - { - $value = new MagicianCall(); - - $this->getPropertyAccessor()->getValue($value, 'foobar', 'bam'); + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); } - - public function testGetValueReadsMagicCall() - { - $propertyAccessor = new PropertyAccessor(true); - $object = new MagicianCall(); - $object->setMagicProperty('foobar'); - - $this->assertSame('foobar', $propertyAccessor->getValue($object, 'magicProperty')); - } - - public function testGetValueReadsMagicCallThatReturnsConstant() - { - $propertyAccessor = new PropertyAccessor(true); - $object = new MagicianCall(); - - $this->assertNull($propertyAccessor->getValue($object, 'MagicProperty')); - } - - public function testSetValueUpdatesMagicCall() - { - $propertyAccessor = new PropertyAccessor(true); - $object = new MagicianCall(); - - $propertyAccessor->setValue($object, 'magicProperty', 'foobar'); - - $this->assertEquals('foobar', $object->getMagicProperty()); - } - } From 20e6bf8f49e607325fa9b2df62de592ec404a1bf Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 28 Mar 2014 17:07:20 +0100 Subject: [PATCH 005/323] [PropertyAccess] Refactored PropertyAccessorCollectionTest --- .../Component/PropertyAccess/CHANGELOG.md | 3 + .../PropertyAccess/PropertyAccessor.php | 8 +- .../Tests/PropertyAccessorCollectionTest.php | 150 +++--------------- .../Tests/PropertyAccessorTest.php | 19 ++- 4 files changed, 46 insertions(+), 134 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index fb5ebfa55034a..bfe3d51459ecd 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG ------ * allowed non alpha numeric characters in second level and deeper object properties names + * [BC BREAK] when accessing an index on an object that does not implement + ArrayAccess, a NoSuchIndexException is now thrown instead of the + semantically wrong NoSuchPropertyException 2.3.0 ------ diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index f5183a109e574..66671c5be079a 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -160,12 +160,12 @@ private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $pr * * @return mixed The value of the key * - * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array + * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array */ private function &readIndex(&$array, $index) { if (!$array instanceof \ArrayAccess && !is_array($array)) { - throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); + throw new NoSuchIndexException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); } // Use an array instead of an object since performance is very crucial here @@ -271,12 +271,12 @@ private function &readProperty(&$object, $property) * @param string|integer $index The index to write at * @param mixed $value The value to write * - * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array + * @throws NoSuchIndexException If the array does not implement \ArrayAccess or it is not an array */ private function writeIndex(&$array, $index, $value) { if (!$array instanceof \ArrayAccess && !is_array($array)) { - throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); + throw new NoSuchIndexException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); } $array[$index] = $value; diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index b0f75aa366613..808c9e1449721 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -47,19 +47,6 @@ public function getAxes() } } -class PropertyAccessorCollectionTest_CarCustomSingular -{ - public function addFoo($axis) {} - - public function removeFoo($axis) {} - - public function getAxes() {} -} - -class PropertyAccessorCollectionTest_Engine -{ -} - class PropertyAccessorCollectionTest_CarOnlyAdder { public function addAxis($axis) {} @@ -79,13 +66,6 @@ class PropertyAccessorCollectionTest_CarNoAdderAndRemover public function getAxes() {} } -class PropertyAccessorCollectionTest_CarNoAdderAndRemoverWithProperty -{ - protected $axes = array(); - - public function getAxes() {} -} - class PropertyAccessorCollectionTest_CompositeCar { public function getStructure() {} @@ -116,52 +96,34 @@ protected function setUp() abstract protected function getCollection(array $array); - public function testGetValueReadsArrayAccess() + public function getValidPropertyPaths() { - $object = $this->getCollection(array('firstName' => 'Bernhard')); - - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[firstName]')); - } - - public function testGetValueReadsNestedArrayAccess() - { - $object = $this->getCollection(array('person' => array('firstName' => 'Bernhard'))); - - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[person][firstName]')); + return array( + array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), + array(array('person' => array('firstName' => 'Bernhard')), '[person][firstName]', 'Bernhard'), + ); } /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @dataProvider getValidPropertyPaths */ - public function testGetValueThrowsExceptionIfArrayAccessExpected() - { - $this->propertyAccessor->getValue(new \stdClass(), '[firstName]'); - } - - public function testSetValueUpdatesArrayAccess() - { - $object = $this->getCollection(array()); - - $this->propertyAccessor->setValue($object, '[firstName]', 'Bernhard'); - - $this->assertEquals('Bernhard', $object['firstName']); - } - - public function testSetValueUpdatesNestedArrayAccess() + public function testGetValue(array $array, $path, $value) { - $object = $this->getCollection(array()); + $collection = $this->getCollection($array); - $this->propertyAccessor->setValue($object, '[person][firstName]', 'Bernhard'); - - $this->assertEquals('Bernhard', $object['person']['firstName']); + $this->assertSame($value, $this->propertyAccessor->getValue($collection, $path)); } /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @dataProvider getValidPropertyPaths */ - public function testSetValueThrowsExceptionIfArrayAccessExpected() + public function testSetValue(array $array, $path) { - $this->propertyAccessor->setValue(new \stdClass(), '[firstName]', 'Bernhard'); + $collection = $this->getCollection($array); + + $this->propertyAccessor->setValue($collection, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path)); } public function testSetValueCallsAdderAndRemoverForCollections() @@ -210,32 +172,9 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() $this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter); } - public function testSetValueCallsCustomAdderAndRemover() - { - $this->markTestSkipped('This feature is temporarily disabled as of 2.1'); - - $car = $this->getMock(__CLASS__.'_CarCustomSingular'); - $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); - $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - - $car->expects($this->at(0)) - ->method('getAxes') - ->will($this->returnValue($axesBefore)); - $car->expects($this->at(1)) - ->method('removeFoo') - ->with('fourth'); - $car->expects($this->at(2)) - ->method('addFoo') - ->with('first'); - $car->expects($this->at(3)) - ->method('addFoo') - ->with('third'); - - $this->propertyAccessor->setValue($car, 'axes|foo', $axesAfter); - } - /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @expectedExceptionMessage Found the public method "addAxis()", but did not find a public "removeAxis()" on class Mock_PropertyAccessorCollectionTest_CarOnlyAdder */ public function testSetValueFailsIfOnlyAdderFound() { @@ -252,6 +191,7 @@ public function testSetValueFailsIfOnlyAdderFound() /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @expectedExceptionMessage Found the public method "removeAxis()", but did not find a public "addAxis()" on class Mock_PropertyAccessorCollectionTest_CarOnlyRemover */ public function testSetValueFailsIfOnlyRemoverFound() { @@ -267,58 +207,14 @@ public function testSetValueFailsIfOnlyRemoverFound() } /** - * @dataProvider noAdderRemoverData + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()", "addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover */ - public function testNoAdderAndRemoverThrowsSensibleError($car, $path, $message) - { - $axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - - try { - $this->propertyAccessor->setValue($car, $path, $axes); - $this->fail('An expected exception was not thrown!'); - } catch (ExceptionInterface $e) { - $this->assertEquals($message, $e->getMessage()); - } - } - - public function noAdderRemoverData() + public function testSetValueFailsIfNoAdderAndNoRemoverFound() { - $data = array(); - $car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover'); - $propertyPath = 'axes'; - $expectedMessage = sprintf( - 'Neither the property "axes" nor one of the methods "addAx()", '. - '"addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have '. - 'public access in class "%s".', - get_class($car) - ); - $data[] = array($car, $propertyPath, $expectedMessage); - - /* - Temporarily disabled in 2.1 - - $propertyPath = new PropertyPath('axes|boo'); - $expectedMessage = sprintf( - 'Neither element "axes" nor method "setAxes()" exists in class ' - .'"%s", nor could adders and removers be found based on the ' - .'passed singular: %s', - get_class($car), - 'boo' - ); - $data[] = array($car, $propertyPath, $expectedMessage); - */ - - $car = $this->getMock(__CLASS__.'_CarNoAdderAndRemoverWithProperty'); - $propertyPath = 'axes'; - $expectedMessage = sprintf( - 'Neither the property "axes" nor one of the methods "addAx()", '. - '"addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have '. - 'public access in class "%s".', - get_class($car) - ); - $data[] = array($car, $propertyPath, $expectedMessage); + $axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - return $data; + $this->propertyAccessor->setValue($car, 'axes', $axes); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 3c13e50bb2101..2d8b97dc579a2 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -31,7 +31,6 @@ protected function setUp() public function getValidPropertyPaths() { - return array( array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'), array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'), @@ -65,7 +64,6 @@ public function getValidPropertyPaths() public function getPathsWithMissingProperty() { - return array( array((object) array('firstName' => 'Bernhard'), 'lastName'), array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.lastName'), @@ -86,7 +84,6 @@ public function getPathsWithMissingProperty() public function getPathsWithMissingIndex() { - return array( array(array('firstName' => 'Bernhard'), '[lastName]'), array(array(), '[index][lastName]'), @@ -131,6 +128,14 @@ public function testGetValueThrowsExceptionIfIndexNotFoundAndIndexExceptionsEnab $this->propertyAccessor->getValue($objectOrArray, $path); } + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testGetValueThrowsExceptionIfNotArrayAccess() + { + $this->propertyAccessor->getValue(new \stdClass(), '[index]'); + } + public function testGetValueReadsMagicGet() { $this->assertSame('Bernhard', $this->propertyAccessor->getValue(new TestClassMagicGet('Bernhard'), 'magicProperty')); @@ -229,6 +234,14 @@ public function testSetValueThrowsNoExceptionIfIndexNotFoundAndIndexExceptionsEn $this->assertSame('Updated', $this->propertyAccessor->getValue($objectOrArray, $path)); } + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testSetValueThrowsExceptionIfNotArrayAccess() + { + $this->propertyAccessor->setValue(new \stdClass(), '[index]', 'Updated'); + } + public function testSetValueUpdatesMagicSet() { $author = new TestClassMagicGet('Bernhard'); From 6d2af217aafd03e3f1600ce0ebc9c30cf0a7fc70 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 28 Mar 2014 17:21:37 +0100 Subject: [PATCH 006/323] [PropertyAccess] Added isReadable() and isWritable() --- UPGRADE-2.5.md | 43 +++++- .../Component/PropertyAccess/CHANGELOG.md | 1 + .../PropertyAccess/PropertyAccessor.php | 126 +++++++++++++++- .../PropertyAccessorInterface.php | 31 +++- .../Tests/PropertyAccessorCollectionTest.php | 54 ++++++- .../Tests/PropertyAccessorTest.php | 136 ++++++++++++++++++ 6 files changed, 382 insertions(+), 9 deletions(-) diff --git a/UPGRADE-2.5.md b/UPGRADE-2.5.md index e3b581b5b9dfd..f1afa9495bebd 100644 --- a/UPGRADE-2.5.md +++ b/UPGRADE-2.5.md @@ -45,6 +45,47 @@ Form { ``` +PropertyAccess +-------------- + + * The methods `isReadable()` and `isWritable()` were added to + `PropertyAccessorInterface`. If you implemented this interface in your own + code, you should add these two methods. + + * The methods `getValue()` and `setValue()` now throw an + `NoSuchIndexException` instead of a `NoSuchPropertyException` when an index + is accessed on an object that does not implement `ArrayAccess`. If you catch + this exception in your code, you should adapt the catch statement: + + Before: + + ```php + $object = new \stdClass(); + + try { + $propertyAccessor->getValue($object, '[index]'); + $propertyAccessor->setValue($object, '[index]', 'New value'); + } catch (NoSuchPropertyException $e) { + // ... + } + ``` + + After: + + ```php + $object = new \stdClass(); + + try { + $propertyAccessor->getValue($object, '[index]'); + $propertyAccessor->setValue($object, '[index]', 'New value'); + } catch (NoSuchIndexException $e) { + // ... + } + ``` + + A `NoSuchPropertyException` is still thrown when a non-existing property is + accessed on an object or an array. + Validator --------- @@ -56,7 +97,7 @@ Validator After: - Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be + Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be valid. This is the default behaviour. Strict email validation has to be explicitly activated in the configuration file by adding diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index bfe3d51459ecd..631c9d725622a 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * [BC BREAK] when accessing an index on an object that does not implement ArrayAccess, a NoSuchIndexException is now thrown instead of the semantically wrong NoSuchPropertyException + * [BC BREAK] added isReadable() and isWritable() to PropertyAccessorInterface 2.3.0 ------ diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 66671c5be079a..b21866bd685ba 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -105,6 +105,84 @@ public function setValue(&$objectOrArray, $propertyPath, $value) } } + /** + * {@inheritdoc} + */ + public function isReadable($objectOrArray, $propertyPath) + { + if (is_string($propertyPath)) { + $propertyPath = new PropertyPath($propertyPath); + } elseif (!$propertyPath instanceof PropertyPathInterface) { + throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + } + + try { + $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->throwExceptionOnInvalidIndex); + + return true; + } catch (NoSuchIndexException $e) { + return false; + } catch (NoSuchPropertyException $e) { + return false; + } catch (UnexpectedTypeException $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function isWritable($objectOrArray, $propertyPath, $value) + { + if (is_string($propertyPath)) { + $propertyPath = new PropertyPath($propertyPath); + } elseif (!$propertyPath instanceof PropertyPathInterface) { + throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + } + + try { + $propertyValues = $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); + $overwrite = true; + + // Add the root object to the list + array_unshift($propertyValues, array( + self::VALUE => $objectOrArray, + self::IS_REF => true, + )); + + for ($i = count($propertyValues) - 1; $i >= 0; --$i) { + $objectOrArray = $propertyValues[$i][self::VALUE]; + + if ($overwrite) { + if (!is_object($objectOrArray) && !is_array($objectOrArray)) { + return false; + } + + $property = $propertyPath->getElement($i); + + if ($propertyPath->isIndex($i)) { + if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { + return false; + } + } else { + if (!$this->isPropertyWritable($objectOrArray, $property, $value)) { + return false; + } + } + } + + $value = $objectOrArray; + $overwrite = !$propertyValues[$i][self::IS_REF]; + } + + return true; + } catch (NoSuchIndexException $e) { + return false; + } catch (NoSuchPropertyException $e) { + return false; + } + } + /** * Reads the path from an object up to a given path index. * @@ -357,9 +435,9 @@ private function writeProperty(&$object, $property, $singular, $value) $setter = 'set'.$this->camelize($property); $classHasProperty = $reflClass->hasProperty($property); - if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { + if ($this->isMethodAccessible($reflClass, $setter, 1)) { $object->$setter($value); - } elseif ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) { + } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) { $object->$property = $value; } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { $object->$property = $value; @@ -370,7 +448,7 @@ private function writeProperty(&$object, $property, $singular, $value) // returns true, consequently the following line will result in a // fatal error. $object->$property = $value; - } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) { + } elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { // we call the getter and hope the __call do the job $object->$setter($value); } else { @@ -385,6 +463,38 @@ private function writeProperty(&$object, $property, $singular, $value) } } + private function isPropertyWritable($object, $property, $value) + { + if (!is_object($object)) { + throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + } + + $reflClass = new \ReflectionClass($object); + $plural = $this->camelize($property); + + // Any of the two methods is required, but not yet known + $singulars = (array) StringUtil::singularify($plural); + + if (is_array($value) || $value instanceof \Traversable) { + try { + if (null !== $this->findAdderAndRemover($reflClass, $singulars)) { + return true; + } + } catch (NoSuchPropertyException $e) { + return false; + } + } + + $setter = 'set'.$this->camelize($property); + $classHasProperty = $reflClass->hasProperty($property); + + return $this->isMethodAccessible($reflClass, $setter, 1) + || $this->isMethodAccessible($reflClass, '__set', 2) + || ($classHasProperty && $reflClass->getProperty($property)->isPublic()) + || (!$classHasProperty && property_exists($object, $property)) + || ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)); + } + /** * Camelizes a given string. * @@ -409,6 +519,8 @@ private function camelize($string) */ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) { + $exception = null; + foreach ($singulars as $singular) { $addMethod = 'add'.$singular; $removeMethod = 'remove'.$singular; @@ -420,8 +532,8 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula return array($addMethod, $removeMethod); } - if ($addMethodFound xor $removeMethodFound) { - throw new NoSuchPropertyException(sprintf( + if ($addMethodFound xor $removeMethodFound && null === $exception) { + $exception = new NoSuchPropertyException(sprintf( 'Found the public method "%s()", but did not find a public "%s()" on class %s', $addMethodFound ? $addMethod : $removeMethod, $addMethodFound ? $removeMethod : $addMethod, @@ -430,6 +542,10 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula } } + if (null !== $exception) { + throw $exception; + } + return null; } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php index 1eed7c7b074cf..b9da0e4937d64 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php @@ -19,7 +19,7 @@ interface PropertyAccessorInterface { /** - * Sets the value at the end of the property path of the object + * Sets the value at the end of the property path of the object graph. * * Example: * @@ -50,7 +50,7 @@ interface PropertyAccessorInterface public function setValue(&$objectOrArray, $propertyPath, $value); /** - * Returns the value at the end of the property path of the object + * Returns the value at the end of the property path of the object graph. * * Example: * @@ -78,4 +78,31 @@ public function setValue(&$objectOrArray, $propertyPath, $value); * @throws Exception\NoSuchPropertyException If a property does not exist or is not public. */ public function getValue($objectOrArray, $propertyPath); + + /** + * Returns whether a value can be written at a given property path. + * + * Whenever this method returns true, {@link setValue()} is guaranteed not + * to throw an exception when called with the same arguments. + * + * @param object|array $objectOrArray The object or array to check + * @param string|PropertyPathInterface $propertyPath The property path to check + * @param mixed $value The value to set at the end of the property path + * + * @return Boolean Whether the value can be set + */ + public function isWritable($objectOrArray, $propertyPath, $value); + + /** + * Returns whether a property path can be read from an object graph. + * + * Whenever this method returns true, {@link getValue()} is guaranteed not + * to throw an exception when called with the same arguments. + * + * @param object|array $objectOrArray The object or array to check + * @param string|PropertyPathInterface $propertyPath The property path to check + * + * @return Boolean Whether the property path can be read + */ + public function isReadable($objectOrArray, $propertyPath); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 808c9e1449721..cd51f2601c39d 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -210,11 +210,63 @@ public function testSetValueFailsIfOnlyRemoverFound() * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()", "addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover */ - public function testSetValueFailsIfNoAdderAndNoRemoverFound() + public function testSetValueFailsIfNoAdderNorRemoverFound() { $car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover'); $axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); $this->propertyAccessor->setValue($car, 'axes', $axes); } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsReadable(array $array, $path) + { + $collection = $this->getCollection($array); + + $this->assertTrue($this->propertyAccessor->isReadable($collection, $path)); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsWritable(array $array, $path) + { + $collection = $this->getCollection($array); + + $this->assertTrue($this->propertyAccessor->isWritable($collection, $path, 'Updated')); + } + + public function testIsWritableReturnsTrueIfAdderAndRemoverExists() + { + $car = $this->getMock(__CLASS__.'_Car'); + $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + + $this->assertTrue($this->propertyAccessor->isWritable($car, 'axes', $axes)); + } + + public function testIsWritableReturnsFalseIfOnlyAdderExists() + { + $car = $this->getMock(__CLASS__.'_CarOnlyAdder'); + $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + + $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes)); + } + + public function testIsWritableReturnsFalseIfOnlyRemoverExists() + { + $car = $this->getMock(__CLASS__.'_CarOnlyRemover'); + $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + + $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes)); + } + + public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists() + { + $car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover'); + $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + + $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes)); + } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 2d8b97dc579a2..a40c2d9ffa668 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -301,4 +301,140 @@ public function testSetValueThrowsExceptionIfEmpty() $this->propertyAccessor->setValue($value, 'foobar', 'bam'); } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsReadable($objectOrArray, $path) + { + $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingProperty + */ + public function testIsReadableReturnsFalseIfPropertyNotFound($objectOrArray, $path) + { + $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsReadableReturnsTrueIfIndexNotFound($objectOrArray, $path) + { + // Non-existing indices can be read. In this case, null is returned + $this->assertTrue($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsReadableReturnsFalseIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + { + $this->propertyAccessor = new PropertyAccessor(false, true); + + // When exceptions are enabled, non-existing indices cannot be read + $this->assertFalse($this->propertyAccessor->isReadable($objectOrArray, $path)); + } + + public function testIsReadableRecognizesMagicGet() + { + $this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicGet('Bernhard'), 'magicProperty')); + } + + public function testIsReadableDoesNotRecognizeMagicCallByDefault() + { + $this->assertFalse($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + public function testIsReadableRecognizesMagicCallIfEnabled() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertTrue($this->propertyAccessor->isReadable(new TestClassMagicCall('Bernhard'), 'magicCallProperty')); + } + + public function testIsReadableThrowsExceptionIfNotObjectOrArray() + { + $this->assertFalse($this->propertyAccessor->isReadable('baz', 'foobar')); + } + + public function testIsReadableThrowsExceptionIfNull() + { + $this->assertFalse($this->propertyAccessor->isReadable(null, 'foobar')); + } + + public function testIsReadableThrowsExceptionIfEmpty() + { + $this->assertFalse($this->propertyAccessor->isReadable('', 'foobar')); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsWritable($objectOrArray, $path) + { + $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated')); + } + + /** + * @dataProvider getPathsWithMissingProperty + */ + public function testIsWritableReturnsFalseIfPropertyNotFound($objectOrArray, $path) + { + $this->assertFalse($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated')); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsWritableReturnsTrueIfIndexNotFound($objectOrArray, $path) + { + // Non-existing indices can be written. Arrays are created on-demand. + $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated')); + } + + /** + * @dataProvider getPathsWithMissingIndex + */ + public function testIsWritableReturnsTrueIfIndexNotFoundAndIndexExceptionsEnabled($objectOrArray, $path) + { + $this->propertyAccessor = new PropertyAccessor(false, true); + + // Non-existing indices can be written even if exceptions are enabled + $this->assertTrue($this->propertyAccessor->isWritable($objectOrArray, $path, 'Updated')); + } + + public function testIsWritableRecognizesMagicSet() + { + $this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicGet('Bernhard'), 'magicProperty', 'Updated')); + } + + public function testIsWritableDoesNotRecognizeMagicCallByDefault() + { + $this->assertFalse($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty', 'Updated')); + } + + public function testIsWritableRecognizesMagicCallIfEnabled() + { + $this->propertyAccessor = new PropertyAccessor(true); + + $this->assertTrue($this->propertyAccessor->isWritable(new TestClassMagicCall('Bernhard'), 'magicCallProperty', 'Updated')); + } + + public function testIsWritableThrowsExceptionIfNotObjectOrArray() + { + $this->assertFalse($this->propertyAccessor->isWritable('baz', 'foobar', 'Updated')); + } + + public function testIsWritableThrowsExceptionIfNull() + { + $this->assertFalse($this->propertyAccessor->isWritable(null, 'foobar', 'Updated')); + } + + public function testIsWritableThrowsExceptionIfEmpty() + { + $this->assertFalse($this->propertyAccessor->isWritable('', 'foobar', 'Updated')); + } } From ce0efb189f9a47c13ffd8091611fe0d9004194ad Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 28 Mar 2014 18:03:44 +0100 Subject: [PATCH 007/323] [Form] ObjectChoiceList now compares choices by their value, if a value path is given --- src/Symfony/Component/Form/CHANGELOG.md | 2 + .../Extension/Core/ChoiceList/ChoiceList.php | 4 +- .../Core/ChoiceList/ObjectChoiceList.php | 74 ++++++++++++ .../Core/ChoiceList/ObjectChoiceListTest.php | 113 ++++++++++++++++++ 4 files changed, 191 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index c210f9e7924ac..f7071663d4846 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -11,6 +11,8 @@ CHANGELOG * [BC BREAK] added two optional parameters to FormInterface::getErrors() and changed the method to return a Symfony\Component\Form\FormErrorIterator instance instead of an array + * ObjectChoiceList now compares choices by their value, if a value path is + given 2.4.0 ----- diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 413f7c4f741a6..3266ca38e2f78 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -41,14 +41,14 @@ class ChoiceList implements ChoiceListInterface * * @var array */ - private $choices = array(); + protected $choices = array(); /** * The choice values with the indices of the matching choices as keys. * * @var array */ - private $values = array(); + protected $values = array(); /** * The preferred view objects as hierarchy containing also the choice groups diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index 0f1437db21649..9c6c3ab598927 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -148,6 +148,80 @@ protected function initialize($choices, array $labels, array $preferredChoices) parent::initialize($choices, $labels, $preferredChoices); } + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $choices) + { + if (!$this->valuePath) { + return parent::getValuesForChoices($choices); + } + + // Use the value path to compare the choices + $choices = $this->fixChoices($choices); + $values = array(); + + foreach ($choices as $i => $givenChoice) { + // Ignore non-readable choices + if (!is_object($givenChoice) && !is_array($givenChoice)) { + continue; + } + + $givenValue = (string) $this->propertyAccessor->getValue($givenChoice, $this->valuePath); + + foreach ($this->values as $value) { + if ($value === $givenValue) { + $values[$i] = $value; + unset($choices[$i]); + + if (0 === count($choices)) { + break 2; + } + } + } + } + + return $values; + } + + /** + * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. + */ + public function getIndicesForChoices(array $choices) + { + if (!$this->valuePath) { + return parent::getIndicesForChoices($choices); + } + + // Use the value path to compare the choices + $choices = $this->fixChoices($choices); + $indices = array(); + + foreach ($choices as $i => $givenChoice) { + // Ignore non-readable choices + if (!is_object($givenChoice) && !is_array($givenChoice)) { + continue; + } + + $givenValue = (string) $this->propertyAccessor->getValue($givenChoice, $this->valuePath); + + foreach ($this->values as $j => $value) { + if ($value === $givenValue) { + $indices[$i] = $j; + unset($choices[$i]); + + if (0 === count($choices)) { + break 2; + } + } + } + } + + return $indices; + } + /** * Creates a new unique value for this choice. * diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php index 27effd9f5c2c0..655101ca1a784 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -185,6 +185,119 @@ public function testInitArrayThrowsExceptionIfToStringNotFound() ); } + public function testGetIndicesForChoicesWithValuePath() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + // Compare by value, not by identity + $choices = array(clone $this->obj1, clone $this->obj2); + $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForChoicesWithValuePathPreservesKeys() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(5 => clone $this->obj1, 8 => clone $this->obj2); + $this->assertSame(array(5 => $this->index1, 8 => $this->index2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForChoicesWithValuePathPreservesOrder() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(clone $this->obj2, clone $this->obj1); + $this->assertSame(array($this->index2, $this->index1), $this->list->getIndicesForChoices($choices)); + } + + public function testGetIndicesForChoicesWithValuePathIgnoresNonExistingChoices() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(clone $this->obj1, clone $this->obj2, 'foobar'); + $this->assertSame(array($this->index1, $this->index2), $this->list->getIndicesForChoices($choices)); + } + + public function testGetValuesForChoicesWithValuePath() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(clone $this->obj1, clone $this->obj2); + $this->assertSame(array('A', 'B'), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesWithValuePathPreservesKeys() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(5 => clone $this->obj1, 8 => clone $this->obj2); + $this->assertSame(array(5 => 'A', 8 => 'B'), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesWithValuePathPreservesOrder() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(clone $this->obj2, clone $this->obj1); + $this->assertSame(array('B', 'A'), $this->list->getValuesForChoices($choices)); + } + + public function testGetValuesForChoicesWithValuePathIgnoresNonExistingChoices() + { + $this->list = new ObjectChoiceList( + array($this->obj1, $this->obj2, $this->obj3, $this->obj4), + 'name', + array(), + null, + 'name' + ); + + $choices = array(clone $this->obj1, clone $this->obj2, 'foobar'); + $this->assertSame(array('A', 'B'), $this->list->getValuesForChoices($choices)); + } + /** * @return \Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface */ From 4262707e5aeee5b67448ba2fb422d2f6c8074445 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 28 Mar 2014 19:21:04 +0100 Subject: [PATCH 008/323] [PropertyAccess] Fixed CS and added missing documentation --- .../PropertyAccess/PropertyAccessor.php | 21 +++++++++++-------- .../Tests/PropertyAccessorCollectionTest.php | 2 -- .../Tests/PropertyAccessorTest.php | 1 - 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index b21866bd685ba..8c86d88303083 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -33,7 +33,7 @@ class PropertyAccessor implements PropertyAccessorInterface /** * @var Boolean */ - private $throwExceptionOnInvalidIndex; + private $ignoreInvalidIndices; /** * Should not be used by application code. Use @@ -42,7 +42,7 @@ class PropertyAccessor implements PropertyAccessorInterface public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false) { $this->magicCall = $magicCall; - $this->throwExceptionOnInvalidIndex = $throwExceptionOnInvalidIndex; + $this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex; } /** @@ -56,7 +56,7 @@ public function getValue($objectOrArray, $propertyPath) throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); } - $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->throwExceptionOnInvalidIndex); + $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); return $propertyValues[count($propertyValues) - 1][self::VALUE]; } @@ -117,7 +117,7 @@ public function isReadable($objectOrArray, $propertyPath) } try { - $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->throwExceptionOnInvalidIndex); + $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); return true; } catch (NoSuchIndexException $e) { @@ -186,15 +186,18 @@ public function isWritable($objectOrArray, $propertyPath, $value) /** * Reads the path from an object up to a given path index. * - * @param object|array $objectOrArray The object or array to read from - * @param PropertyPathInterface $propertyPath The property path to read - * @param integer $lastIndex The index up to which should be read + * @param object|array $objectOrArray The object or array to read from + * @param PropertyPathInterface $propertyPath The property path to read + * @param integer $lastIndex The index up to which should be read + * @param Boolean $ignoreInvalidIndices Whether to ignore invalid indices + * or throw an exception * * @return array The values read in the path. * * @throws UnexpectedTypeException If a value within the path is neither object nor array. + * @throws NoSuchIndexException If a non-existing index is accessed */ - private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $throwExceptionOnInvalidIndex = false) + private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $ignoreInvalidIndices = true) { $propertyValues = array(); @@ -209,7 +212,7 @@ private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $pr // Create missing nested arrays on demand if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { - if ($throwExceptionOnInvalidIndex) { + if (!$ignoreInvalidIndices) { throw new NoSuchIndexException(sprintf('Cannot read property "%s". Available properties are "%s"', $property, print_r(array_keys($objectOrArray), true))); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index cd51f2601c39d..18211c822b19c 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -11,9 +11,7 @@ namespace Symfony\Component\PropertyAccess\Tests; -use Symfony\Component\PropertyAccess\Exception\ExceptionInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; -use Symfony\Component\PropertyAccess\StringUtil; class PropertyAccessorCollectionTest_Car { diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index a40c2d9ffa668..a6b09fa0ab7a0 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -13,7 +13,6 @@ use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; -use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet; From e79b3a9755bb68c8c61fc08f38b44a29f0c2037e Mon Sep 17 00:00:00 2001 From: Eduardo Gulias Davis Date: Sat, 29 Mar 2014 12:07:57 +0100 Subject: [PATCH 009/323] Change in validator.email service alias to match the validator FQCN --- .../Bundle/FrameworkBundle/Resources/config/validator.xml | 2 +- src/Symfony/Component/Validator/Constraints/Email.php | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 9a30a2065de34..930300f75c5d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -73,7 +73,7 @@ - + diff --git a/src/Symfony/Component/Validator/Constraints/Email.php b/src/Symfony/Component/Validator/Constraints/Email.php index e59d28d9a9124..c079db1b3f039 100644 --- a/src/Symfony/Component/Validator/Constraints/Email.php +++ b/src/Symfony/Component/Validator/Constraints/Email.php @@ -26,12 +26,4 @@ class Email extends Constraint public $checkMX = false; public $checkHost = false; public $strict = null; - - /** - * {@inheritDoc} - */ - public function validatedBy() - { - return 'validator.email'; - } } From 9aee2ad999c78bddf3b5eae52c8a3fc918cb2c48 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sun, 30 Mar 2014 18:19:15 +0200 Subject: [PATCH 010/323] [PropertyAccess] Removed the argument $value from isWritable() To keep isWritable() and setValue() consistent, the exception thrown when only an adder was present, but no remover (or vice versa), was removed. --- .../Exception/InvalidArgumentException.php | 21 ++ .../PropertyAccess/PropertyAccessor.php | 203 ++++++++++-------- .../PropertyAccessorInterface.php | 17 +- .../Tests/PropertyAccessorCollectionTest.php | 36 +--- 4 files changed, 143 insertions(+), 134 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Exception/InvalidArgumentException.php diff --git a/src/Symfony/Component/PropertyAccess/Exception/InvalidArgumentException.php b/src/Symfony/Component/PropertyAccess/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..47bc7e150dd18 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/InvalidArgumentException.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\PropertyAccess\Exception; + +/** + * Base InvalidArgumentException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 8c86d88303083..c52b598cb9391 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\PropertyAccess; +use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; @@ -53,7 +54,12 @@ public function getValue($objectOrArray, $propertyPath) if (is_string($propertyPath)) { $propertyPath = new PropertyPath($propertyPath); } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + throw new InvalidArgumentException(sprintf( + 'The property path should be a string or an instance of '. + '"Symfony\Component\PropertyAccess\PropertyPathInterface". '. + 'Got: "%s"', + is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) + )); } $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); @@ -69,7 +75,12 @@ public function setValue(&$objectOrArray, $propertyPath, $value) if (is_string($propertyPath)) { $propertyPath = new PropertyPath($propertyPath); } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + throw new InvalidArgumentException(sprintf( + 'The property path should be a string or an instance of '. + '"Symfony\Component\PropertyAccess\PropertyPathInterface". '. + 'Got: "%s"', + is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) + )); } $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); @@ -90,13 +101,11 @@ public function setValue(&$objectOrArray, $propertyPath, $value) } $property = $propertyPath->getElement($i); - //$singular = $propertyPath->singulars[$i]; - $singular = null; if ($propertyPath->isIndex($i)) { $this->writeIndex($objectOrArray, $property, $value); } else { - $this->writeProperty($objectOrArray, $property, $singular, $value); + $this->writeProperty($objectOrArray, $property, $value); } } @@ -113,7 +122,12 @@ public function isReadable($objectOrArray, $propertyPath) if (is_string($propertyPath)) { $propertyPath = new PropertyPath($propertyPath); } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + throw new InvalidArgumentException(sprintf( + 'The property path should be a string or an instance of '. + '"Symfony\Component\PropertyAccess\PropertyPathInterface". '. + 'Got: "%s"', + is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) + )); } try { @@ -132,12 +146,17 @@ public function isReadable($objectOrArray, $propertyPath) /** * {@inheritdoc} */ - public function isWritable($objectOrArray, $propertyPath, $value) + public function isWritable($objectOrArray, $propertyPath) { if (is_string($propertyPath)) { $propertyPath = new PropertyPath($propertyPath); } elseif (!$propertyPath instanceof PropertyPathInterface) { - throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + throw new InvalidArgumentException(sprintf( + 'The property path should be a string or an instance of '. + '"Symfony\Component\PropertyAccess\PropertyPathInterface". '. + 'Got: "%s"', + is_object($propertyPath) ? get_class($propertyPath) : gettype($propertyPath) + )); } try { @@ -165,13 +184,12 @@ public function isWritable($objectOrArray, $propertyPath, $value) return false; } } else { - if (!$this->isPropertyWritable($objectOrArray, $property, $value)) { + if (!$this->isPropertyWritable($objectOrArray, $property)) { return false; } } } - $value = $objectOrArray; $overwrite = !$propertyValues[$i][self::IS_REF]; } @@ -346,7 +364,7 @@ private function &readProperty(&$object, $property) } /** - * Sets the value of the property at the given index in the path + * Sets the value of an index in a given array-accessible value. * * @param \ArrayAccess|array $array An array or \ArrayAccess object to write to * @param string|integer $index The index to write at @@ -364,74 +382,33 @@ private function writeIndex(&$array, $index, $value) } /** - * Sets the value of the property at the given index in the path + * Sets the value of a property in the given object * - * @param object|array $object The object or array to write to - * @param string $property The property to write - * @param string|null $singular The singular form of the property name or null - * @param mixed $value The value to write + * @param object $object The object to write to + * @param string $property The property to write + * @param mixed $value The value to write * * @throws NoSuchPropertyException If the property does not exist or is not * public. */ - private function writeProperty(&$object, $property, $singular, $value) + private function writeProperty(&$object, $property, $value) { - $guessedAdders = ''; - if (!is_object($object)) { throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } $reflClass = new \ReflectionClass($object); $plural = $this->camelize($property); - - // Any of the two methods is required, but not yet known - $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); + $singulars = (array) StringUtil::singularify($plural); if (is_array($value) || $value instanceof \Traversable) { $methods = $this->findAdderAndRemover($reflClass, $singulars); + // Use addXxx() and removeXxx() to write the collection if (null !== $methods) { - // At this point the add and remove methods have been found - // Use iterator_to_array() instead of clone in order to prevent side effects - // see https://github.com/symfony/symfony/issues/4670 - $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; - $itemToRemove = array(); - $propertyValue = $this->readProperty($object, $property); - $previousValue = $propertyValue[self::VALUE]; - - if (is_array($previousValue) || $previousValue instanceof \Traversable) { - foreach ($previousValue as $previousItem) { - foreach ($value as $key => $item) { - if ($item === $previousItem) { - // Item found, don't add - unset($itemsToAdd[$key]); - - // Next $previousItem - continue 2; - } - } - - // Item not found, add to remove list - $itemToRemove[] = $previousItem; - } - } - - foreach ($itemToRemove as $item) { - call_user_func(array($object, $methods[1]), $item); - } - - foreach ($itemsToAdd as $item) { - call_user_func(array($object, $methods[0]), $item); - } + $this->writeCollection($object, $property, $value, $methods[0], $methods[1]); return; - } else { - // It is sufficient to include only the adders in the error - // message. If the user implements the adder but not the remover, - // an exception will be thrown in findAdderAndRemover() that - // the remover has to be implemented as well. - $guessedAdders = '"add'.implode('()", "add', $singulars).'()", '; } } @@ -459,43 +436,98 @@ private function writeProperty(&$object, $property, $singular, $value) 'Neither the property "%s" nor one of the methods %s"%s()", '. '"__set()" or "__call()" exist and have public access in class "%s".', $property, - $guessedAdders, + implode('', array_map(function ($singular) { + return '"add'.$singular.'()"/"remove'.$singular.'()", '; + }, $singulars)), $setter, $reflClass->name )); } } - private function isPropertyWritable($object, $property, $value) + /** + * Adjusts a collection-valued property by calling add*() and remove*() + * methods. + * + * @param object $object The object to write to + * @param string $property The property to write + * @param array|\Traversable $collection The collection to write + * @param string $addMethod The add*() method + * @param string $removeMethod The remove*() method + */ + private function writeCollection($object, $property, $collection, $addMethod, $removeMethod) { - if (!is_object($object)) { - throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + // At this point the add and remove methods have been found + // Use iterator_to_array() instead of clone in order to prevent side effects + // see https://github.com/symfony/symfony/issues/4670 + $itemsToAdd = is_object($collection) ? iterator_to_array($collection) : $collection; + $itemToRemove = array(); + $propertyValue = $this->readProperty($object, $property); + $previousValue = $propertyValue[self::VALUE]; + + if (is_array($previousValue) || $previousValue instanceof \Traversable) { + foreach ($previousValue as $previousItem) { + foreach ($collection as $key => $item) { + if ($item === $previousItem) { + // Item found, don't add + unset($itemsToAdd[$key]); + + // Next $previousItem + continue 2; + } + } + + // Item not found, add to remove list + $itemToRemove[] = $previousItem; + } } - $reflClass = new \ReflectionClass($object); - $plural = $this->camelize($property); + foreach ($itemToRemove as $item) { + call_user_func(array($object, $removeMethod), $item); + } - // Any of the two methods is required, but not yet known - $singulars = (array) StringUtil::singularify($plural); + foreach ($itemsToAdd as $item) { + call_user_func(array($object, $addMethod), $item); + } + } - if (is_array($value) || $value instanceof \Traversable) { - try { - if (null !== $this->findAdderAndRemover($reflClass, $singulars)) { - return true; - } - } catch (NoSuchPropertyException $e) { - return false; - } + /** + * Returns whether a property is writable in the given object. + * + * @param object $object The object to write to + * @param string $property The property to write + * + * @return Boolean Whether the property is writable + */ + private function isPropertyWritable($object, $property) + { + if (!is_object($object)) { + return false; } + $reflClass = new \ReflectionClass($object); + $setter = 'set'.$this->camelize($property); $classHasProperty = $reflClass->hasProperty($property); - return $this->isMethodAccessible($reflClass, $setter, 1) + if ($this->isMethodAccessible($reflClass, $setter, 1) || $this->isMethodAccessible($reflClass, '__set', 2) || ($classHasProperty && $reflClass->getProperty($property)->isPublic()) || (!$classHasProperty && property_exists($object, $property)) - || ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)); + || ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2))) { + return true; + } + + $plural = $this->camelize($property); + + // Any of the two methods is required, but not yet known + $singulars = (array) StringUtil::singularify($plural); + + if (null !== $this->findAdderAndRemover($reflClass, $singulars)) { + return true; + } + + return false; } /** @@ -517,8 +549,6 @@ private function camelize($string) * @param array $singulars The singular form of the property name or null * * @return array|null An array containing the adder and remover when found, null otherwise - * - * @throws NoSuchPropertyException If the property does not exist */ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) { @@ -534,19 +564,6 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula if ($addMethodFound && $removeMethodFound) { return array($addMethod, $removeMethod); } - - if ($addMethodFound xor $removeMethodFound && null === $exception) { - $exception = new NoSuchPropertyException(sprintf( - 'Found the public method "%s()", but did not find a public "%s()" on class %s', - $addMethodFound ? $addMethod : $removeMethod, - $addMethodFound ? $removeMethod : $addMethod, - $reflClass->name - )); - } - } - - if (null !== $exception) { - throw $exception; } return null; diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php index b9da0e4937d64..7e1cd24ae8a51 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php @@ -43,9 +43,10 @@ interface PropertyAccessorInterface * @param string|PropertyPathInterface $propertyPath The property path to modify * @param mixed $value The value to set at the end of the property path * - * @throws Exception\NoSuchPropertyException If a property does not exist or is not public. - * @throws Exception\UnexpectedTypeException If a value within the path is neither object - * nor array + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\NoSuchPropertyException If a property does not exist or is not public. + * @throws Exception\UnexpectedTypeException If a value within the path is neither object + * nor array */ public function setValue(&$objectOrArray, $propertyPath, $value); @@ -75,7 +76,8 @@ public function setValue(&$objectOrArray, $propertyPath, $value); * * @return mixed The value at the end of the property path * - * @throws Exception\NoSuchPropertyException If a property does not exist or is not public. + * @throws Exception\InvalidArgumentException If the property path is invalid + * @throws Exception\NoSuchPropertyException If a property does not exist or is not public. */ public function getValue($objectOrArray, $propertyPath); @@ -87,11 +89,12 @@ public function getValue($objectOrArray, $propertyPath); * * @param object|array $objectOrArray The object or array to check * @param string|PropertyPathInterface $propertyPath The property path to check - * @param mixed $value The value to set at the end of the property path * * @return Boolean Whether the value can be set + * + * @throws Exception\InvalidArgumentException If the property path is invalid */ - public function isWritable($objectOrArray, $propertyPath, $value); + public function isWritable($objectOrArray, $propertyPath); /** * Returns whether a property path can be read from an object graph. @@ -103,6 +106,8 @@ public function isWritable($objectOrArray, $propertyPath, $value); * @param string|PropertyPathInterface $propertyPath The property path to check * * @return Boolean Whether the property path can be read + * + * @throws Exception\InvalidArgumentException If the property path is invalid */ public function isReadable($objectOrArray, $propertyPath); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 18211c822b19c..7e4a49247c09e 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -172,41 +172,7 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - * @expectedExceptionMessage Found the public method "addAxis()", but did not find a public "removeAxis()" on class Mock_PropertyAccessorCollectionTest_CarOnlyAdder - */ - public function testSetValueFailsIfOnlyAdderFound() - { - $car = $this->getMock(__CLASS__.'_CarOnlyAdder'); - $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); - $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - - $car->expects($this->any()) - ->method('getAxes') - ->will($this->returnValue($axesBefore)); - - $this->propertyAccessor->setValue($car, 'axes', $axesAfter); - } - - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - * @expectedExceptionMessage Found the public method "removeAxis()", but did not find a public "addAxis()" on class Mock_PropertyAccessorCollectionTest_CarOnlyRemover - */ - public function testSetValueFailsIfOnlyRemoverFound() - { - $car = $this->getMock(__CLASS__.'_CarOnlyRemover'); - $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); - $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - - $car->expects($this->any()) - ->method('getAxes') - ->will($this->returnValue($axesBefore)); - - $this->propertyAccessor->setValue($car, 'axes', $axesAfter); - } - - /** - * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()", "addAxe()", "addAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover + * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()"/"removeAx()", "addAxe()"/"removeAxe()", "addAxis()"/"removeAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover */ public function testSetValueFailsIfNoAdderNorRemoverFound() { From 25cdc68d363855ee0744ac9494891a84f23bba40 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 12 Feb 2014 12:38:50 +0100 Subject: [PATCH 011/323] [Validator] Refactored ValidatorTest and ValidationVisitorTest into an abstract validator test class --- UPGRADE-2.5.md | 25 +- src/Symfony/Component/Validator/CHANGELOG.md | 2 + .../Validator/Constraints/Callback.php | 2 +- .../Validator/Constraints/GroupSequence.php | 41 +- .../Validator/Mapping/ClassMetadata.php | 17 +- .../Validator/Tests/AbstractValidatorTest.php | 1236 +++++++++++++++++ .../Constraints/CallbackValidatorTest.php | 4 +- .../Tests/Constraints/GroupSequenceTest.php | 66 + .../Validator/Tests/Fixtures/Entity.php | 8 +- .../Fixtures/GroupSequenceProviderEntity.php | 8 +- .../Validator/Tests/Fixtures/Reference.php | 13 + .../Validator/Tests/ValidationVisitorTest.php | 564 -------- .../Validator/Tests/ValidatorTest.php | 259 +--- 13 files changed, 1410 insertions(+), 835 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php diff --git a/UPGRADE-2.5.md b/UPGRADE-2.5.md index e3b581b5b9dfd..6ce3e86af546a 100644 --- a/UPGRADE-2.5.md +++ b/UPGRADE-2.5.md @@ -56,10 +56,11 @@ Validator After: - Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be + Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be valid. This is the default behaviour. Strict email validation has to be explicitly activated in the configuration file by adding + ``` framework: //... @@ -68,7 +69,29 @@ Validator //... ``` + Also you have to add to your composer.json: + ``` "egulias/email-validator": "1.1.*" ``` + + * `ClassMetadata::getGroupSequence()` now returns `GroupSequence` instances + instead of an array. The sequence implements `\Traversable`, `\ArrayAccess` + and `\Countable`, so in most cases you should be fine. If you however use the + sequence with PHP's `array_*()` functions, you should cast it to an array + first using `iterator_to_array()`: + + Before: + + ``` + $sequence = $metadata->getGroupSequence(); + $result = array_map($callback, $sequence); + ``` + + After: + + ``` + $sequence = iterator_to_array($metadata->getGroupSequence()); + $result = array_map($callback, $sequence); + ``` diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 3eba52930f3f7..ecdf0cf366a6e 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG * deprecated `ApcCache` in favor of `DoctrineCache` * added `DoctrineCache` to adapt any Doctrine cache + * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` + * changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array 2.4.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Callback.php b/src/Symfony/Component/Validator/Constraints/Callback.php index 01aeb6ddb7982..18cd7b3e92c1a 100644 --- a/src/Symfony/Component/Validator/Constraints/Callback.php +++ b/src/Symfony/Component/Validator/Constraints/Callback.php @@ -71,6 +71,6 @@ public function getDefaultOption() */ public function getTargets() { - return self::CLASS_CONSTRAINT; + return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); } } diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 304fab8c943b0..61f72c6231cf9 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -20,7 +20,7 @@ * * @api */ -class GroupSequence +class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable { /** * The members of the sequence @@ -30,6 +30,43 @@ class GroupSequence public function __construct(array $groups) { - $this->groups = $groups['value']; + // Support for Doctrine annotations + $this->groups = isset($groups['value']) ? $groups['value'] : $groups; + } + + public function getIterator() + { + return new \ArrayIterator($this->groups); + } + + public function offsetExists($offset) + { + return isset($this->groups[$offset]); + } + + public function offsetGet($offset) + { + return $this->groups[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null !== $offset) { + $this->groups[$offset] = $value; + + return; + } + + $this->groups[] = $value; + } + + public function offsetUnset($offset) + { + unset($this->groups[$offset]); + } + + public function count() + { + return count($this->groups); } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index b7e003ec3da40..8bba73a01f13e 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; use Symfony\Component\Validator\ClassBasedInterface; @@ -330,27 +331,31 @@ public function getConstrainedProperties() /** * Sets the default group sequence for this class. * - * @param array $groups An array of group names + * @param array $groupSequence An array of group names * * @return ClassMetadata * * @throws GroupDefinitionException */ - public function setGroupSequence(array $groups) + public function setGroupSequence($groupSequence) { if ($this->isGroupSequenceProvider()) { throw new GroupDefinitionException('Defining a static group sequence is not allowed with a group sequence provider'); } - if (in_array(Constraint::DEFAULT_GROUP, $groups, true)) { + if (is_array($groupSequence)) { + $groupSequence = new GroupSequence($groupSequence); + } + + if (in_array(Constraint::DEFAULT_GROUP, $groupSequence->groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is not allowed in group sequences', Constraint::DEFAULT_GROUP)); } - if (!in_array($this->getDefaultGroup(), $groups, true)) { + if (!in_array($this->getDefaultGroup(), $groupSequence->groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is missing in the group sequence', $this->getDefaultGroup())); } - $this->groupSequence = $groups; + $this->groupSequence = $groupSequence; return $this; } @@ -368,7 +373,7 @@ public function hasGroupSequence() /** * Returns the default group sequence for this class. * - * @return array An array of group names + * @return GroupSequence The group sequence or null */ public function getGroupSequence() { diff --git a/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php new file mode 100644 index 0000000000000..33e39c5345e04 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php @@ -0,0 +1,1236 @@ + + * + * 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\Constraint; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; +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\Entity; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\ValidatorInterface; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase +{ + const ENTITY_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Entity'; + + const REFERENCE_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Reference'; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var FakeMetadataFactory + */ + public $metadataFactory; + + /** + * @var ClassMetadata + */ + public $metadata; + + /** + * @var ClassMetadata + */ + public $referenceMetadata; + + protected function setUp() + { + $this->metadataFactory = new FakeMetadataFactory(); + $this->validator = $this->createValidator($this->metadataFactory); + $this->metadata = new ClassMetadata(self::ENTITY_CLASS); + $this->referenceMetadata = new ClassMetadata(self::REFERENCE_CLASS); + $this->metadataFactory->addMetadata($this->metadata); + $this->metadataFactory->addMetadata($this->referenceMetadata); + } + + protected function tearDown() + { + $this->metadataFactory = null; + $this->validator = null; + $this->metadata = null; + $this->referenceMetadata = null; + } + + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + + public function testClassConstraint() + { + $test = $this; + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testPropertyConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->firstName = 'Bernhard'; + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('firstName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('firstName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testGetterConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->setLastName('Schussek'); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('lastName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('lastName', $context->getPropertyName()); + $test->assertSame('lastName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Schussek', $context->getValue()); + $test->assertSame('Schussek', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addGetterConstraint('lastName', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('lastName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Schussek', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testReferenceClassConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testReferencePropertyConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference->value = 'Foobar'; + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->referenceMetadata->getPropertyMetadata('value'); + + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertSame('value', $context->getPropertyName()); + $test->assertSame('reference.value', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Foobar', $context->getValue()); + $test->assertSame('Foobar', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addPropertyConstraint('value', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference.value', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Foobar', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testReferenceGetterConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference->setPrivateValue('Bamboo'); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->referenceMetadata->getPropertyMetadata('privateValue'); + + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertSame('privateValue', $context->getPropertyName()); + $test->assertSame('reference.privateValue', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bamboo', $context->getValue()); + $test->assertSame('Bamboo', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addPropertyConstraint('privateValue', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference.privateValue', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bamboo', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testsIgnoreNullReference() + { + $entity = new Entity(); + $entity->reference = null; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testFailOnScalarReferences() + { + $entity = new Entity(); + $entity->reference = 'string'; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->validator->validate($entity); + } + + public function testArrayReference() + { + $test = $this; + $entity = new Entity(); + $entity->reference = array('key' => new Reference()); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference['key'], $context->getValue()); + $test->assertSame($entity->reference['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + // https://github.com/symfony/symfony/issues/6246 + public function testRecursiveArrayReference() + { + $test = $this; + $entity = new Entity(); + $entity->reference = array(2 => array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference[2]['key'], $context->getValue()); + $test->assertSame($entity->reference[2]['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference[2]['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testArrayTraversalCannotBeDisabled() + { + $entity = new Entity(); + $entity->reference = array('key' => new Reference()); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testRecursiveArrayTraversalCannotBeDisabled() + { + $entity = new Entity(); + $entity->reference = array(2 => array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testIgnoreScalarsDuringArrayTraversal() + { + $entity = new Entity(); + $entity->reference = array('string', 1234); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testIgnoreNullDuringArrayTraversal() + { + $entity = new Entity(); + $entity->reference = array(null); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testTraversableReference() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference['key'], $context->getValue()); + $test->assertSame($entity->reference['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testDisableTraversableTraversal() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + /** + * @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->validator->validate($entity, 'Default', ''); + } + + public function testNoRecursiveTraversableTraversal() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testEnableRecursiveTraversableTraversal() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference[2]['key'], $context->getValue()); + $test->assertSame($entity->reference[2]['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => true, + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference[2]['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateProperty() + { + $test = $this; + $entity = new Entity(); + $entity->firstName = 'Bernhard'; + $entity->setLastName('Schussek'); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('firstName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Other violation'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('lastName', new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validateProperty($entity, 'firstName', 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('firstName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidatePropertyFailsIfPropertiesNotSupported() + { + // $metadata does not implement PropertyMetadataContainerInterface + $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + + $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->with('VALUE') + ->will($this->returnValue($metadata)); + $validator = $this->createValidator($metadataFactory); + + $validator->validateProperty('VALUE', 'someProperty'); + } + + public function testValidatePropertyValue() + { + $test = $this; + $entity = new Entity(); + $entity->setLastName('Schussek'); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('firstName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Other violation'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('lastName', new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validatePropertyValue( + $entity, + 'firstName', + 'Bernhard', + 'Group' + ); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('firstName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidatePropertyValueFailsIfPropertiesNotSupported() + { + // $metadata does not implement PropertyMetadataContainerInterface + $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + + $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->with('VALUE') + ->will($this->returnValue($metadata)); + $validator = $this->createValidator($metadataFactory); + + $validator->validatePropertyValue('VALUE', 'someProperty', 'someValue'); + } + + public function testValidateValue() + { + $test = $this; + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->assertNull($context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertNull($context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame('Bernhard', $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $constraint = new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + )); + + $violations = $this->validator->validateValue('Bernhard', $constraint, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame('Bernhard', $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidateValueRejectsValid() + { + $this->validator->validateValue(new Entity(), new Valid()); + } + + public function testAddCustomizedViolation() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation( + 'Message %param%', + array('%param%' => 'value'), + 'Invalid value', + 2, + 'Code' + ); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); + $this->assertSame(2, $violations[0]->getMessagePluralization()); + $this->assertSame('Code', $violations[0]->getCode()); + } + + public function testValidateObjectOnlyOncePerGroup() + { + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference2 = $entity->reference; + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->addPropertyConstraint('reference2', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testValidateDifferentObjectsSeparately() + { + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference2 = new Reference(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->addPropertyConstraint('reference2', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(2, $violations); + } + + public function testValidateSingleGroup() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 2', + ))); + + $violations = $this->validator->validate($entity, 'Group 2'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testValidateMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 2', + ))); + + $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(2, $violations); + } + + public function testNoDuplicateValidationIfConstraintInMultipleGroups() + { + $this->markTestSkipped('Currently not supported'); + + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testGroupSequenceAbortsAfterFailedGroup() + { + $this->markTestSkipped('Currently not supported'); + + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 2'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); + $violations = $this->validator->validate($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message 1', $violations[0]->getMessage()); + } + + public function testGroupSequenceIncludesReferences() + { + $this->markTestSkipped('Currently not supported'); + + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 2'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 1', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 2', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $violations = $this->validator->validate($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Reference violation 1', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupByGroupSequenceObject() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupByGroupSequenceArray() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Default group'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in group sequence'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Default', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 1', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Default group', $violations[0]->getMessage()); + } + + public function testValidateCustomGroupWhenDefaultGroupWasReplaced() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in other group'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in group sequence'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Other Group', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 1', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Other Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in other group', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupWithObjectFromGroupSequenceProvider() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); + $entity = new GroupSequenceProviderEntity($sequence); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $metadata = new ClassMetadata(get_class($entity)); + $metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + $metadata->setGroupSequenceProvider(true); + + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() + { + $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); + $entity = new GroupSequenceProviderEntity($sequence); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $metadata = new ClassMetadata(get_class($entity)); + $metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + $metadata->setGroupSequenceProvider(true); + + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testGetMetadataFactory() + { + $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index cdcd49bb58ed8..e0317823d52c9 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ExecutionContext; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\CallbackValidator; @@ -320,8 +321,9 @@ public function testExpectEitherCallbackOrMethods() public function testConstraintGetTargets() { $constraint = new Callback(array('foo')); + $targets = array(Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT); - $this->assertEquals('class', $constraint->getTargets()); + $this->assertEquals($targets, $constraint->getTargets()); } // Should succeed. Needed when defining constraints as annotations. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php new file mode 100644 index 0000000000000..83275d1c72733 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php @@ -0,0 +1,66 @@ + + * + * 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\Validator\Constraints\GroupSequence; + +/** + * @author Bernhard Schussek + */ +class GroupSequenceTest extends \PHPUnit_Framework_TestCase +{ + public function testCreate() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertSame(array('Group 1', 'Group 2'), $sequence->groups); + } + + public function testCreateDoctrineStyle() + { + $sequence = new GroupSequence(array('value' => array('Group 1', 'Group 2'))); + + $this->assertSame(array('Group 1', 'Group 2'), $sequence->groups); + } + + public function testIterate() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertSame(array('Group 1', 'Group 2'), iterator_to_array($sequence)); + } + + public function testCount() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertCount(2, $sequence); + } + + public function testArrayAccess() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertSame('Group 1', $sequence[0]); + $this->assertSame('Group 2', $sequence[1]); + $this->assertTrue(isset($sequence[0])); + $this->assertFalse(isset($sequence[2])); + unset($sequence[0]); + $this->assertFalse(isset($sequence[0])); + $sequence[] = 'Group 3'; + $this->assertTrue(isset($sequence[2])); + $this->assertSame('Group 3', $sequence[2]); + $sequence[0] = 'Group 1'; + $this->assertTrue(isset($sequence[0])); + $this->assertSame('Group 1', $sequence[0]); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php index fbd879a94eaf1..d841f5dc9d78e 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php @@ -32,9 +32,10 @@ class Entity extends EntityParent implements EntityInterface * }) * @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%") */ - protected $firstName; + public $firstName; protected $lastName; public $reference; + public $reference2; private $internal; public $data = 'Overridden data'; @@ -48,6 +49,11 @@ public function getInternal() return $this->internal.' from getter'; } + public function setLastName($lastName) + { + $this->lastName = $lastName; + } + /** * @Assert\NotNull */ diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php b/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php index ef3711104ad43..2b0beaf9adf98 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php @@ -22,15 +22,15 @@ class GroupSequenceProviderEntity implements GroupSequenceProviderInterface public $firstName; public $lastName; - protected $groups = array(); + protected $sequence = array(); - public function setGroups($groups) + public function __construct($sequence) { - $this->groups = $groups; + $this->sequence = $sequence; } public function getGroupSequence() { - return $this->groups; + return $this->sequence; } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php b/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php index f8ea173e019aa..af29735924379 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php @@ -13,4 +13,17 @@ class Reference { + public $value; + + private $privateValue; + + public function setPrivateValue($privateValue) + { + $this->privateValue = $privateValue; + } + + public function getPrivateValue() + { + return $this->privateValue; + } } diff --git a/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php b/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php deleted file mode 100644 index 2868f57a82cd6..0000000000000 --- a/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php +++ /dev/null @@ -1,564 +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\Tests\Fixtures\FakeMetadataFactory; -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Tests\Fixtures\Reference; -use Symfony\Component\Validator\DefaultTranslator; -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(), new DefaultTranslator()); - $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', - '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', - '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', - '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', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // generated by the reference - new ConstraintViolation( - 'Failed', - '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', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // generated by the reference - new ConstraintViolation( - 'Failed', - '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', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // generated by the reference - new ConstraintViolation( - 'Failed', - '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', - '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', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - // https://github.com/symfony/symfony/issues/6246 - public function testValidateCascadedPropertyRecursesArraysByDefault() - { - $entity = new Entity(); - $entity->reference = array( - 'key' => 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()); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference[key][nested]', - $entity->reference['key']['nested'] - ), - )); - - $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', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - new ConstraintViolation( - 'Failed', - '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/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php index 85a61e4816da8..52bdbea519af2 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -11,266 +11,15 @@ 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\FakeMetadataFactory; -use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Mapping\ClassMetadata; -class ValidatorTest extends \PHPUnit_Framework_TestCase +class ValidatorTest extends AbstractValidatorTest { - /** - * @var FakeMetadataFactory - */ - private $metadataFactory; - - /** - * @var Validator - */ - private $validator; - - protected function setUp() - { - $this->metadataFactory = new FakeMetadataFactory(); - $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - } - - protected function tearDown() - { - $this->metadataFactory = null; - $this->validator = null; - } - - public function testValidateDefaultGroup() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Custom', - ))); - $this->metadataFactory->addMetadata($metadata); - - // Only the constraint of group "Default" failed - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'firstName', - '' - )); - - $this->assertEquals($violations, $this->validator->validate($entity)); - } - - public function testValidateOneGroup() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Custom', - ))); - $this->metadataFactory->addMetadata($metadata); - - // Only the constraint of group "Custom" failed - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'lastName', - '' - )); - - $this->assertEquals($violations, $this->validator->validate($entity, 'Custom')); - } - - public function testValidateMultipleGroups() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint(array( - 'groups' => 'First', - ))); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Second', - ))); - $this->metadataFactory->addMetadata($metadata); - - // The constraints of both groups failed - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'firstName', - '' - )); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'lastName', - '' - )); - - $result = $this->validator->validate($entity, array('First', 'Second')); - - $this->assertEquals($violations, $result); - } - - public function testValidateGroupSequenceProvider() - { - $entity = new GroupSequenceProviderEntity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint(array( - 'groups' => 'First', - ))); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Second', - ))); - $metadata->setGroupSequenceProvider(true); - $this->metadataFactory->addMetadata($metadata); - - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'firstName', - '' - )); - - $entity->setGroups(array('First')); - $result = $this->validator->validate($entity); - $this->assertEquals($violations, $result); - - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'lastName', - '' - )); - - $entity->setGroups(array('Second')); - $result = $this->validator->validate($entity); - $this->assertEquals($violations, $result); - - $entity->setGroups(array()); - $result = $this->validator->validate($entity); - $this->assertEquals(new ConstraintViolationList(), $result); - } - - public function testValidateProperty() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $this->metadataFactory->addMetadata($metadata); - - $result = $this->validator->validateProperty($entity, 'firstName'); - - $this->assertCount(1, $result); - - $result = $this->validator->validateProperty($entity, 'lastName'); - - $this->assertCount(0, $result); - } - - public function testValidatePropertyValue() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $this->metadataFactory->addMetadata($metadata); - - $result = $this->validator->validatePropertyValue(get_class($entity), 'firstName', 'Bernhard'); - - $this->assertCount(1, $result); - } - - public function testValidateValue() - { - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Bernhard', - '', - 'Bernhard' - )); - - $this->assertEquals($violations, $this->validator->validateValue('Bernhard', new FailingConstraint())); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidateValueRejectsValid() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $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(), new DefaultTranslator()); - - $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(), new DefaultTranslator()); - - $this->validator->validatePropertyValue('VALUE', 'someProperty', 'propertyValue'); - } - - public function testGetMetadataFactory() + protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $this->assertInstanceOf( - 'Symfony\Component\Validator\MetadataFactoryInterface', - $this->validator->getMetadataFactory() - ); + return new Validator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); } } From a6ed4cae5daeebf9dec20bba359f0f1c9be9c178 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 14:12:00 +0100 Subject: [PATCH 012/323] [Validator] Prototype of the traverser implementation --- .../Validator/Constraints/GroupSequence.php | 18 ++ .../Validator/Context/ExecutionContext.php | 174 ++++++++++++++++++ .../Context/ExecutionContextInterface.php | 154 ++++++++++++++++ .../Context/ExecutionContextManager.php | 130 +++++++++++++ .../ExecutionContextManagerInterface.php | 34 ++++ .../Context/LegacyExecutionContext.php | 41 +++++ .../Validator/Group/GroupManagerInterface.php | 21 +++ .../Mapping/ClassMetadataInterface.php | 44 +++++ .../Validator/Mapping/MetadataInterface.php | 57 ++++++ .../Mapping/PropertyMetadataInterface.php | 46 +++++ .../Validator/Mapping/ValueMetadata.php | 46 +++++ .../Component/Validator/Node/ClassNode.php | 41 +++++ src/Symfony/Component/Validator/Node/Node.php | 37 ++++ .../Component/Validator/Node/PropertyNode.php | 37 ++++ .../Component/Validator/Node/ValueNode.php | 20 ++ .../NodeTraverser/AbstractVisitor.php | 37 ++++ .../Validator/NodeTraverser/NodeTraverser.php | 166 +++++++++++++++++ .../NodeTraverser/NodeTraverserInterface.php | 32 ++++ .../NodeTraverser/NodeVisitorInterface.php | 29 +++ .../Validator/TraversingValidatorTest.php | 41 +++++ .../Validator/Validator/AbstractValidator.php | 155 ++++++++++++++++ .../Validator/ContextualValidator.php | 123 +++++++++++++ .../ContextualValidatorInterface.php | 26 +++ .../Validator/Validator/LegacyValidator.php | 27 +++ .../Validator/Validator/NodeValidator.php | 155 ++++++++++++++++ .../Validator/Validator/Validator.php | 72 ++++++++ .../Validator/ValidatorInterface.php | 88 +++++++++ 27 files changed, 1851 insertions(+) create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContext.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextInterface.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManager.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php create mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContext.php create mode 100644 src/Symfony/Component/Validator/Group/GroupManagerInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/MetadataInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/ValueMetadata.php create mode 100644 src/Symfony/Component/Validator/Node/ClassNode.php create mode 100644 src/Symfony/Component/Validator/Node/Node.php create mode 100644 src/Symfony/Component/Validator/Node/PropertyNode.php create mode 100644 src/Symfony/Component/Validator/Node/ValueNode.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Validator/AbstractValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/ContextualValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php create mode 100644 src/Symfony/Component/Validator/Validator/LegacyValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/NodeValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/Validator.php create mode 100644 src/Symfony/Component/Validator/Validator/ValidatorInterface.php diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 61f72c6231cf9..7985b6cc9726b 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Traversable; + /** * Annotation for group sequences * @@ -28,6 +30,22 @@ class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable */ public $groups; + /** + * The group under which cascaded objects are validated when validating + * this sequence. + * + * By default, cascaded objects are validated in each of the groups of + * the sequence. + * + * If a class has a group sequence attached, that sequence replaces the + * "Default" group. When validating that class in the "Default" group, the + * group sequence is used instead, but still the "Default" group should be + * cascaded to other objects. + * + * @var string|GroupSequence + */ + public $cascadedGroup; + public function __construct(array $groups) { // Support for Doctrine annotations diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php new file mode 100644 index 0000000000000..09dc2644e2a1f --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ExecutionContext implements ExecutionContextInterface +{ + private $root; + + private $violations; + + /** + * @var Node + */ + private $node; + + /** + * @var \SplStack + */ + private $nodeStack; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var GroupManagerInterface + */ + private $groupManager; + + public function __construct(MetadataFactoryInterface $metadataFactory, ValidatorInterface $validator, GroupManagerInterface $groupManager) + { + $this->metadataFactory = $metadataFactory; + $this->validator = $validator; + $this->groupManager = $groupManager; + $this->violations = new ConstraintViolationList(); + } + + public function pushNode(Node $node) + { + if (null === $this->node) { + $this->root = $node->value; + } else { + $this->nodeStack->push($this->node); + } + + $this->node = $node; + } + + public function popNode() + { + $poppedNode = $this->node; + + if (0 === count($this->nodeStack)) { + $this->node = null; + + return $poppedNode; + } + + if (1 === count($this->nodeStack)) { + $this->nodeStack->pop(); + $this->node = null; + + return $poppedNode; + } + + $this->nodeStack->pop(); + $this->node = $this->nodeStack->top(); + + return $poppedNode; + } + + public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + { + } + + public function buildViolation($message) + { + + } + + public function getMetadataFor($object) + { + + } + + public function getViolations() + { + return $this->violations; + } + + public function getRoot() + { + return $this->root; + } + + public function getValue() + { + return $this->node ? $this->node->value : null; + } + + public function getMetadata() + { + return $this->node ? $this->node->metadata : null; + } + + public function getGroup() + { + return $this->groupManager->getCurrentGroup(); + } + + public function getClassName() + { + $metadata = $this->getMetadata(); + + return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null; + } + + public function getPropertyName() + { + $metadata = $this->getMetadata(); + + return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null; + } + + public function getPropertyPath($subPath = '') + { + $propertyPath = $this->node ? $this->node->propertyPath : ''; + + if (strlen($subPath) > 0) { + if ('[' === $subPath{1}) { + return $propertyPath.$subPath; + } + + return $propertyPath ? $propertyPath.'.'.$subPath : $subPath; + } + + return $propertyPath; + } + + /** + * @return ValidatorInterface + */ + public function getValidator() + { + return $this->validator; + } +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php new file mode 100644 index 0000000000000..c7ee62dc23ca8 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ExecutionContextInterface +{ + /** + * @return ValidatorInterface + */ + public function getValidator(); + + /** + * 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. + * + * @api + */ + public function addViolation($message, array $params = array()); + + public function buildViolation($message); + + /** + * 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(); + + public function getMetadataFor($object); + + /** + * 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 = ''); +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php new file mode 100644 index 0000000000000..08442b94e8263 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface +{ + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var ExecutionContext + */ + private $currentContext; + + /** + * @var \SplStack|ExecutionContext[] + */ + private $contextStack; + + public function __construct(MetadataFactoryInterface $metadataFactory, GroupManagerInterface $groupManager) + { + $this->metadataFactory = $metadataFactory; + $this->groupManager = $groupManager; + + $this->reset(); + } + + public function initialize(ValidatorInterface $validator) + { + $this->validator = $validator; + } + + public function startContext() + { + if (null !== $this->currentContext) { + $this->contextStack->push($this->currentContext); + } + + $this->currentContext = new ExecutionContext($this->metadataFactory, $this->validator, $this->groupManager); + + return $this->currentContext; + } + + public function stopContext() + { + $stoppedContext = $this->currentContext; + + if (0 === count($this->contextStack)) { + $this->currentContext = null; + + return $stoppedContext; + } + + if (1 === count($this->contextStack)) { + $this->contextStack->pop(); + $this->currentContext = null; + + return $stoppedContext; + } + + $this->contextStack->pop(); + $this->currentContext = $this->contextStack->top(); + + return $stoppedContext; + } + + public function getCurrentContext() + { + return $this->currentContext; + } + + public function afterTraversal(array $nodes) + { + $this->reset(); + } + + public function enterNode(Node $node) + { + if (null === $this->currentContext) { + // error no context started + } + + $this->currentContext->pushNode($node); + } + + public function leaveNode(Node $node) + { + if (null === $this->currentContext) { + // error no context started + } + + $this->currentContext->popNode(); + } + + private function reset() + { + $this->contextStack = new \SplStack(); + } +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php new file mode 100644 index 0000000000000..0d79eb43bb3de --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ExecutionContextManagerInterface +{ + /** + * @return ExecutionContextInterface The started context + */ + public function startContext(); + + /** + * @return ExecutionContextInterface The stopped context + */ + public function stopContext(); + + /** + * @return ExecutionContextInterface The current context + */ + public function getCurrentContext(); +} diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php new file mode 100644 index 0000000000000..1981e0f00ed47 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface +{ + public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + { + + } + + public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) + { + + } + + public function validateValue($value, $constraints, $subPath = '', $groups = null) + { + + } + + public function getMetadataFactory() + { + + } +} diff --git a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php new file mode 100644 index 0000000000000..94a0ab9544e10 --- /dev/null +++ b/src/Symfony/Component/Validator/Group/GroupManagerInterface.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\Group; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface GroupManagerInterface +{ + public function getCurrentGroup(); +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php new file mode 100644 index 0000000000000..fea3f7f1d7423 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.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\Mapping; + +use Symfony\Component\Validator\ClassBasedInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ClassMetadataInterface extends MetadataInterface, ClassBasedInterface +{ + public function getConstrainedProperties(); + + public function hasPropertyMetadata($property); + + /** + * 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); + + public function hasGroupSequence(); + + public function getGroupSequence(); + + public function isGroupSequenceProvider(); +} diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php new file mode 100644 index 0000000000000..3df0d9bc0d587 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * 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 +{ + /** + * Returns all constraints for a given validation group. + * + * @param string $group The validation group. + * + * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. + */ + public function findConstraints($group); + + public function supportsCascading(); +} diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php new file mode 100644 index 0000000000000..78da11b9074a9 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php @@ -0,0 +1,46 @@ + + * + * 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\ClassBasedInterface; + +/** + * 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, ClassBasedInterface +{ + /** + * Returns the name of the property. + * + * @return string The property name. + */ + public function getPropertyName(); + + /** + * Extracts the value of the property from the given object. + * + * @param mixed $object The object to extract the property value from. + * + * @return mixed The value of the property. + */ + public function getPropertyValue($object); +} diff --git a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php new file mode 100644 index 0000000000000..c51a6fa575dae --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ValueMetadata implements MetadataInterface +{ + /** + * Returns all constraints for a given validation group. + * + * @param string $group The validation group. + * + * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. + */ + public function findConstraints($group) + { + + } + + public function supportsCascading() + { + + } + + public function supportsIteration() + { + + } + + public function supportsRecursiveIteration() + { + + } +} diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php new file mode 100644 index 0000000000000..dfb06dbc0325e --- /dev/null +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ClassNode extends Node +{ + /** + * @var ClassMetadataInterface + */ + public $metadata; + + public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups) + { + if (!is_object($value)) { + // error + } + + parent::__construct( + $value, + $metadata, + $propertyPath, + $groups + ); + } + +} diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php new file mode 100644 index 0000000000000..3dead5623d8ac --- /dev/null +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +abstract class Node +{ + public $value; + + public $metadata; + + public $propertyPath; + + public $groups; + + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups) + { + $this->value = $value; + $this->metadata = $metadata; + $this->propertyPath = $propertyPath; + $this->groups = $groups; + } +} diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php new file mode 100644 index 0000000000000..9424acb59f526 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class PropertyNode extends Node +{ + /** + * @var PropertyMetadataInterface + */ + public $metadata; + + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups) + { + parent::__construct( + $value, + $metadata, + $propertyPath, + $groups + ); + } + +} diff --git a/src/Symfony/Component/Validator/Node/ValueNode.php b/src/Symfony/Component/Validator/Node/ValueNode.php new file mode 100644 index 0000000000000..e0f77e41f19c0 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/ValueNode.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ValueNode extends Node +{ +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php new file mode 100644 index 0000000000000..c03e87c18da5d --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +abstract class AbstractVisitor implements NodeVisitorInterface +{ + public function beforeTraversal(array $nodes) + { + } + + public function afterTraversal(array $nodes) + { + } + + public function enterNode(Node $node) + { + } + + public function leaveNode(Node $node) + { + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php new file mode 100644 index 0000000000000..3d3c7bfe03ac8 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class NodeTraverser implements NodeTraverserInterface +{ + /** + * @var NodeVisitorInterface[] + */ + private $visitors; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + private $traversalStarted = false; + + public function __construct(MetadataFactoryInterface $metadataFactory) + { + $this->visitors = new \SplObjectStorage(); + $this->metadataFactory = $metadataFactory; + } + + public function addVisitor(NodeVisitorInterface $visitor) + { + $this->visitors->attach($visitor); + } + + public function removeVisitor(NodeVisitorInterface $visitor) + { + $this->visitors->detach($visitor); + } + + /** + * {@inheritdoc} + */ + public function traverse(array $nodes) + { + $isTopLevelCall = !$this->traversalStarted; + + if ($isTopLevelCall) { + $this->traversalStarted = true; + + foreach ($this->visitors as $visitor) { + /** @var NodeVisitorInterface $visitor */ + $visitor->beforeTraversal($nodes); + } + } + + foreach ($nodes as $node) { + if ($node instanceof ClassNode) { + $this->traverseClassNode($node); + } else { + $this->traverseNode($node); + } + } + + if ($isTopLevelCall) { + $this->traversalStarted = false; + + foreach ($this->visitors as $visitor) { + /** @var NodeVisitorInterface $visitor */ + $visitor->afterTraversal($nodes); + } + } + } + + private function traverseNode(Node $node) + { + $stopTraversal = false; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->enterNode($node)) { + $stopTraversal = true; + } + } + + // Stop the traversal, but execute the leaveNode() methods anyway to + // perform possible cleanups + if (!$stopTraversal && is_object($node->value) && $node->metadata->supportsCascading()) { + $classMetadata = $this->metadataFactory->getMetadataFor($node->value); + + $this->traverseClassNode(new ClassNode( + $node->value, + $classMetadata, + $node->propertyPath, + $node->groups + )); + } + + foreach ($this->visitors as $visitor) { + $visitor->leaveNode($node); + } + } + + private function traverseClassNode(ClassNode $node) + { + // Replace "Default" group by the group sequence attached to the class + // (if any) + foreach ($node->groups as $key => $group) { + if (Constraint::DEFAULT_GROUP !== $group) { + continue; + } + + if ($node->metadata->hasGroupSequence()) { + $node->groups[$key] = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $node->groups[$key] = $value->getGroupSequence(); + } + + // Cascade the "Default" group when validating the sequence + $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; + + // "Default" group found, abort + break; + } + + $stopTraversal = false; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->enterNode($node)) { + $stopTraversal = true; + } + } + + // Stop the traversal, but execute the leaveNode() methods anyway to + // perform possible cleanups + if (!$stopTraversal) { + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + $this->traverseNode(new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups + )); + } + } + } + + foreach ($this->visitors as $visitor) { + $visitor->leaveNode($node); + } + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php new file mode 100644 index 0000000000000..048a1458b4760 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface NodeTraverserInterface +{ + public function addVisitor(NodeVisitorInterface $visitor); + + public function removeVisitor(NodeVisitorInterface $visitor); + + /** + * @param Node[] $nodes + * + * @return mixed + */ + public function traverse(array $nodes); +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php new file mode 100644 index 0000000000000..0a70cc13fe7da --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface NodeVisitorInterface +{ + public function beforeTraversal(array $nodes); + + public function afterTraversal(array $nodes); + + public function enterNode(Node $node); + + public function leaveNode(Node $node); +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php new file mode 100644 index 0000000000000..4f3a212aa1864 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\Context\ExecutionContextManager; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\AbstractValidatorTest; +use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Validator\Validator; + +class TraversingValidatorTest extends AbstractValidatorTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $validatorFactory = new ConstraintValidatorFactory(); + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($validatorFactory, $nodeTraverser); + $contextManager = new ExecutionContextManager($metadataFactory, $nodeValidator, new DefaultTranslator()); + $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + + $contextManager->initialize($validator); + $nodeValidator->setContextManager($contextManager); + + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php new file mode 100644 index 0000000000000..c0d9f54b52db1 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\ValueMetadata; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeTraverser\ClassNode; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\NodeTraverser\PropertyNode; +use Symfony\Component\Validator\NodeTraverser\ValueNode; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +abstract class AbstractValidator implements ValidatorInterface +{ + /** + * @var NodeTraverserInterface + */ + protected $nodeTraverser; + + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + /** + * @var string + */ + protected $defaultPropertyPath = ''; + + protected $defaultGroups = array(Constraint::DEFAULT_GROUP); + + public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) + { + $this->nodeTraverser = $nodeTraverser; + $this->metadataFactory = $metadataFactory; + } + + /** + * @param ExecutionContextInterface $context + * + * @return ContextualValidatorInterface + */ + public function inContext(ExecutionContextInterface $context) + { + return new ContextualValidator($this->nodeTraverser, $this->metadataFactory, $context); + } + + public function getMetadataFactory() + { + return $this->metadataFactory; + } + + protected function traverseObject($object, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $this->nodeTraverser->traverse(array(new ClassNode( + $object, + $classMetadata, + $this->defaultPropertyPath, + // TODO use cascade group here + $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + ))); + } + + protected function traverseProperty($object, $propertyName, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyValue = $propertyMetadata->getPropertyValue($object); + + $nodes[] = new PropertyNode( + $propertyValue, + $propertyMetadata, + $this->defaultPropertyPath, + $groups + ); + } + + $this->nodeTraverser->traverse($nodes); + } + + protected function traversePropertyValue($object, $propertyName, $value, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $nodes[] = new PropertyNode( + $value, + $propertyMetadata, + $this->defaultPropertyPath, + $groups + ); + } + + $this->nodeTraverser->traverse($nodes); + } + + protected function traverseValue($value, $constraints, $groups = null) + { + $metadata = new ValueMetadata($constraints); + + $this->nodeTraverser->traverse(array(new ValueNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + ))); + } + + protected function normalizeGroups($groups) + { + if (is_array($groups)) { + return $groups; + } + + return array($groups); + } +} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php new file mode 100644 index 0000000000000..560f088522992 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ContextualValidator extends AbstractValidator implements ContextualValidatorInterface +{ + /** + * @var ExecutionContextManagerInterface + */ + private $context; + + public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextInterface $context) + { + parent::__construct($nodeTraverser, $metadataFactory); + + $this->context = $context; + $this->defaultPropertyPath = $context->getPropertyPath(); + $this->defaultGroups = array($context->getGroup()); + } + + public function atPath($subPath) + { + $this->defaultPropertyPath = $this->context->getPropertyPath($subPath); + } + + /** + * Validates a value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value 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. + */ + public function validateObject($object, $groups = null) + { + $this->traverseObject($object, $groups); + + return $this->context->getViolations(); + } + + /** + * Validates a property of a value against its current value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value containing the property. + * @param string $propertyName 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. + */ + public function validateProperty($object, $propertyName, $groups = null) + { + $this->traverseProperty($object, $propertyName, $groups); + + return $this->context->getViolations(); + } + + /** + * Validate a property of a value against a potential value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param string $object The value containing the property. + * @param string $propertyName 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. + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + $this->traversePropertyValue($object, $propertyName, $value, $groups); + + return $this->context->getViolations(); + } + + /** + * Validates a value against a constraint or a list of constraints. + * + * @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 ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateValue($value, $constraints, $groups = null) + { + $this->traverseValue($value, $constraints, $groups); + + return $this->context->getViolations(); + } +} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php new file mode 100644 index 0000000000000..61de8900f1431 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ContextualValidatorInterface extends ValidatorInterface +{ + /** + * @param $subPath + * + * @return ContextualValidatorInterface + */ + public function atPath($subPath); +} diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php new file mode 100644 index 0000000000000..2227318289b8d --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.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\Validator; + +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class LegacyValidator extends Validator implements LegacyValidatorInterface +{ + public function validate($value, $groups = null, $traverse = false, $deep = false) + { + // TODO what about $traverse and $deep? + return $this->validateObject($value, $groups); + } +} diff --git a/src/Symfony/Component/Validator/Validator/NodeValidator.php b/src/Symfony/Component/Validator/Validator/NodeValidator.php new file mode 100644 index 0000000000000..20177341a810c --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/NodeValidator.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class NodeValidator extends AbstractVisitor implements GroupManagerInterface +{ + private $validatedNodes = array(); + + /** + * @var ConstraintValidatorFactoryInterface + */ + private $validatorFactory; + + /** + * @var ExecutionContextManagerInterface + */ + private $contextManager; + + /** + * @var NodeTraverserInterface + */ + private $nodeTraverser; + + private $currentGroup; + + public function __construct(ConstraintValidatorFactoryInterface $validatorFactory, NodeTraverserInterface $nodeTraverser) + { + $this->validatorFactory = $validatorFactory; + $this->nodeTraverser = $nodeTraverser; + } + + public function setContextManager(ExecutionContextManagerInterface $contextManager) + { + $this->contextManager = $contextManager; + } + + public function afterTraversal(array $nodes) + { + $this->validatedNodes = array(); + } + + public function enterNode(Node $node) + { + $cacheKey = $node instanceof ClassNode + ? spl_object_hash($node->value) + : null; + + // if group (=[,G3,G4]) contains group sequence (=) + // then call traverse() with each entry of the group sequence and abort + // if necessary (G1, G2) + // finally call traverse() with remaining entries ([G3,G4]) or + // simply continue traversal (if possible) + + foreach ($node->groups as $group) { + // Validate object nodes only once per group + if (null !== $cacheKey) { + // Use the object hash for group sequences + $groupKey = is_object($group) ? spl_object_hash($group) : $group; + + // Exit, if the object is already validated for the current group + if (isset($this->validatedNodes[$cacheKey][$groupKey])) { + return false; + } + + // Remember validating this object before starting and possibly + // traversing the object graph + $this->validatedNodes[$cacheKey][$groupKey] = true; + } + + // Validate group sequence until a violation is generated + if ($group instanceof GroupSequence) { + // Rename for clarity + $groupSequence = $group; + + // Only evaluate group sequences at class, not at property level + if (!$node instanceof ClassNode) { + continue; + } + + $context = $this->contextManager->getCurrentContext(); + $violationCount = count($context->getViolations()); + + foreach ($groupSequence->groups as $groupInSequence) { + $this->nodeTraverser->traverse(array(new ClassNode( + $node->value, + $node->metadata, + $node->propertyPath, + array($groupInSequence), + array($groupSequence->cascadedGroup ?: $groupInSequence) + ))); + + // Abort sequence validation if a violation was generated + if (count($context->getViolations()) > $violationCount) { + break; + } + } + + // Optimization: If the groups only contain the group sequence, + // we can skip the traversal for the properties of the object + if (1 === count($node->groups)) { + return false; + } + + // We're done for the current loop execution. + continue; + } + + // Validate normal group (non group sequences) + try { + $this->currentGroup = $group; + + foreach ($node->metadata->findConstraints($group) as $constraint) { + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($this->contextManager->getCurrentContext()); + $validator->validate($node->value, $constraint); + } + + $this->currentGroup = null; + } catch (\Exception $e) { + $this->currentGroup = null; + + throw $e; + } + } + + return true; + } + + public function getCurrentGroup() + { + return $this->currentGroup; + } +} diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php new file mode 100644 index 0000000000000..ed222d57c81dc --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class Validator extends AbstractValidator +{ + /** + * @var ExecutionContextManagerInterface + */ + private $contextManager; + + public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextManagerInterface $contextManager) + { + parent::__construct($nodeTraverser, $metadataFactory); + + $this->contextManager = $contextManager; + } + + public function validateObject($object, $groups = null) + { + $this->contextManager->startContext(); + + $this->traverseObject($object, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + + public function validateProperty($object, $propertyName, $groups = null) + { + $this->contextManager->startContext(); + + $this->traverseProperty($object, $propertyName, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + $this->contextManager->startContext(); + + $this->traversePropertyValue($object, $propertyName, $value, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + + public function validateValue($value, $constraints, $groups = null) + { + $this->contextManager->startContext(); + + $this->traverseValue($value, $constraints, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + +} diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php new file mode 100644 index 0000000000000..4f557710c4cf1 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ValidatorInterface +{ + /** + * Validates a value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value 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. + */ + public function validateObject($object, $groups = null); + + /** + * Validates a property of a value against its current value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value containing the property. + * @param string $propertyName 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. + */ + public function validateProperty($object, $propertyName, $groups = null); + + /** + * Validate a property of a value against a potential value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param string $object The value containing the property. + * @param string $propertyName 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. + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null); + + /** + * Validates a value against a constraint or a list of constraints. + * + * @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 ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateValue($value, $constraints, $groups = null); + + /** + * @param ExecutionContextInterface $context + * + * @return ContextualValidatorInterface + */ + public function inContext(ExecutionContextInterface $context); +} From a40189ccb7c90c8919effa68261488103648d4aa Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 14:43:28 +0100 Subject: [PATCH 013/323] [Validator] Decoupled the new classes a bit --- .../Validator/Context/ExecutionContext.php | 14 +- .../Context/ExecutionContextInterface.php | 2 - .../Context/ExecutionContextManager.php | 11 +- .../Validator/NodeTraverser/NodeTraverser.php | 4 +- .../NodeTraverser/NodeTraverserInterface.php | 2 - .../Validator/TraversingValidatorTest.php | 14 +- .../Validator/Validator/AbstractValidator.php | 15 ++- .../Validator/Validator/NodeValidator.php | 121 ++++++++++-------- .../Validator/ValidatorInterface.php | 4 + 9 files changed, 95 insertions(+), 92 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 09dc2644e2a1f..ba4b0cb8018ff 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -15,7 +15,6 @@ use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -39,11 +38,6 @@ class ExecutionContext implements ExecutionContextInterface */ private $nodeStack; - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - /** * @var ValidatorInterface */ @@ -54,9 +48,8 @@ class ExecutionContext implements ExecutionContextInterface */ private $groupManager; - public function __construct(MetadataFactoryInterface $metadataFactory, ValidatorInterface $validator, GroupManagerInterface $groupManager) + public function __construct(ValidatorInterface $validator, GroupManagerInterface $groupManager) { - $this->metadataFactory = $metadataFactory; $this->validator = $validator; $this->groupManager = $groupManager; $this->violations = new ConstraintViolationList(); @@ -105,11 +98,6 @@ public function buildViolation($message) } - public function getMetadataFor($object) - { - - } - public function getViolations() { return $this->violations; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index c7ee62dc23ca8..4a8765f27be5c 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -88,8 +88,6 @@ public function getValue(); */ public function getMetadata(); - public function getMetadataFor($object); - /** * Returns the validation group that is currently being validated. * diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 08442b94e8263..c6b3da0601133 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\Group\GroupManagerInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -23,11 +22,6 @@ */ class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface { - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - /** * @var GroupManagerInterface */ @@ -48,9 +42,8 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex */ private $contextStack; - public function __construct(MetadataFactoryInterface $metadataFactory, GroupManagerInterface $groupManager) + public function __construct(GroupManagerInterface $groupManager) { - $this->metadataFactory = $metadataFactory; $this->groupManager = $groupManager; $this->reset(); @@ -67,7 +60,7 @@ public function startContext() $this->contextStack->push($this->currentContext); } - $this->currentContext = new ExecutionContext($this->metadataFactory, $this->validator, $this->groupManager); + $this->currentContext = new ExecutionContext($this->validator, $this->groupManager); return $this->currentContext; } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 3d3c7bfe03ac8..b1962c263aee8 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Node\PropertyNode; /** * @since %%NextVersion%% diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 048a1458b4760..501c0ee7d1af4 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -25,8 +25,6 @@ public function removeVisitor(NodeVisitorInterface $visitor); /** * @param Node[] $nodes - * - * @return mixed */ public function traverse(array $nodes); } diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php index 4f3a212aa1864..2dc35387c799c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php @@ -15,23 +15,27 @@ use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\AbstractValidatorTest; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\NodeTraverser\NodeVisitor\NodeValidator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Validator\NodeValidator; use Symfony\Component\Validator\Validator\Validator; class TraversingValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $validatorFactory = new ConstraintValidatorFactory(); $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($validatorFactory, $nodeTraverser); - $contextManager = new ExecutionContextManager($metadataFactory, $nodeValidator, new DefaultTranslator()); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + // The context manager needs the validator for passing it to created + // contexts $contextManager->initialize($validator); - $nodeValidator->setContextManager($contextManager); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); $nodeTraverser->addVisitor($contextManager); $nodeTraverser->addVisitor($nodeValidator); diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index c0d9f54b52db1..5401fc6dbe21d 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -16,10 +16,10 @@ use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\ValueMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeTraverser\ClassNode; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\PropertyNode; +use Symfony\Component\Validator\Node\ValueNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; -use Symfony\Component\Validator\NodeTraverser\PropertyNode; -use Symfony\Component\Validator\NodeTraverser\ValueNode; /** * @since %%NextVersion%% @@ -60,9 +60,14 @@ public function inContext(ExecutionContextInterface $context) return new ContextualValidator($this->nodeTraverser, $this->metadataFactory, $context); } - public function getMetadataFactory() + public function getMetadataFor($object) { - return $this->metadataFactory; + return $this->metadataFactory->getMetadataFor($object); + } + + public function hasMetadataFor($object) + { + return $this->metadataFactory->hasMetadataFor($object); } protected function traverseObject($object, $groups = null) diff --git a/src/Symfony/Component/Validator/Validator/NodeValidator.php b/src/Symfony/Component/Validator/Validator/NodeValidator.php index 20177341a810c..7de32e88b13e8 100644 --- a/src/Symfony/Component/Validator/Validator/NodeValidator.php +++ b/src/Symfony/Component/Validator/Validator/NodeValidator.php @@ -26,7 +26,7 @@ */ class NodeValidator extends AbstractVisitor implements GroupManagerInterface { - private $validatedNodes = array(); + private $validatedObjects = array(); /** * @var ConstraintValidatorFactoryInterface @@ -45,25 +45,25 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface private $currentGroup; - public function __construct(ConstraintValidatorFactoryInterface $validatorFactory, NodeTraverserInterface $nodeTraverser) + public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; } - public function setContextManager(ExecutionContextManagerInterface $contextManager) + public function initialize(ExecutionContextManagerInterface $contextManager) { $this->contextManager = $contextManager; } public function afterTraversal(array $nodes) { - $this->validatedNodes = array(); + $this->validatedObjects = array(); } public function enterNode(Node $node) { - $cacheKey = $node instanceof ClassNode + $objectHash = $node instanceof ClassNode ? spl_object_hash($node->value) : null; @@ -75,73 +75,38 @@ public function enterNode(Node $node) foreach ($node->groups as $group) { // Validate object nodes only once per group - if (null !== $cacheKey) { + if (null !== $objectHash) { // Use the object hash for group sequences - $groupKey = is_object($group) ? spl_object_hash($group) : $group; + $groupHash = is_object($group) ? spl_object_hash($group) : $group; // Exit, if the object is already validated for the current group - if (isset($this->validatedNodes[$cacheKey][$groupKey])) { + if (isset($this->validatedObjects[$objectHash][$groupHash])) { return false; } // Remember validating this object before starting and possibly // traversing the object graph - $this->validatedNodes[$cacheKey][$groupKey] = true; + $this->validatedObjects[$objectHash][$groupHash] = true; } // Validate group sequence until a violation is generated - if ($group instanceof GroupSequence) { - // Rename for clarity - $groupSequence = $group; + if (!$group instanceof GroupSequence) { + $this->validateNodeForGroup($node, $group); - // Only evaluate group sequences at class, not at property level - if (!$node instanceof ClassNode) { - continue; - } - - $context = $this->contextManager->getCurrentContext(); - $violationCount = count($context->getViolations()); - - foreach ($groupSequence->groups as $groupInSequence) { - $this->nodeTraverser->traverse(array(new ClassNode( - $node->value, - $node->metadata, - $node->propertyPath, - array($groupInSequence), - array($groupSequence->cascadedGroup ?: $groupInSequence) - ))); - - // Abort sequence validation if a violation was generated - if (count($context->getViolations()) > $violationCount) { - break; - } - } - - // Optimization: If the groups only contain the group sequence, - // we can skip the traversal for the properties of the object - if (1 === count($node->groups)) { - return false; - } - - // We're done for the current loop execution. continue; } - // Validate normal group (non group sequences) - try { - $this->currentGroup = $group; - - foreach ($node->metadata->findConstraints($group) as $constraint) { - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($this->contextManager->getCurrentContext()); - $validator->validate($node->value, $constraint); - } + // Only traverse group sequences at class, not at property level + if (!$node instanceof ClassNode) { + continue; + } - $this->currentGroup = null; - } catch (\Exception $e) { - $this->currentGroup = null; + $this->traverseGroupSequence($node, $group); - throw $e; + // Optimization: If the groups only contain the group sequence, + // we can skip the traversal for the properties of the object + if (1 === count($node->groups)) { + return false; } } @@ -152,4 +117,50 @@ public function getCurrentGroup() { return $this->currentGroup; } + + private function traverseGroupSequence(ClassNode $node, GroupSequence $groupSequence) + { + $context = $this->contextManager->getCurrentContext(); + $violationCount = count($context->getViolations()); + + foreach ($groupSequence->groups as $groupInSequence) { + $this->nodeTraverser->traverse(array(new ClassNode( + $node->value, + $node->metadata, + $node->propertyPath, + array($groupInSequence), + array($groupSequence->cascadedGroup ?: $groupInSequence) + ))); + + // Abort sequence validation if a violation was generated + if (count($context->getViolations()) > $violationCount) { + break; + } + } + } + + /** + * @param Node $node + * @param $group + * + * @throws \Exception + */ + private function validateNodeForGroup(Node $node, $group) + { + try { + $this->currentGroup = $group; + + foreach ($node->metadata->findConstraints($group) as $constraint) { + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($this->contextManager->getCurrentContext()); + $validator->validate($node->value, $constraint); + } + + $this->currentGroup = null; + } catch (\Exception $e) { + $this->currentGroup = null; + + throw $e; + } + } } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 4f557710c4cf1..f02ed79d9b756 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -85,4 +85,8 @@ public function validateValue($value, $constraints, $groups = null); * @return ContextualValidatorInterface */ public function inContext(ExecutionContextInterface $context); + + public function getMetadataFor($object); + + public function hasMetadataFor($object); } From 7e3a41d9db8a609f010ee404ffbf4fe397ea873b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 15:03:04 +0100 Subject: [PATCH 014/323] [Validator] Moved visitors to NodeVisitor namespace --- .../Validator/Context/ExecutionContextManager.php | 2 +- .../Component/Validator/NodeTraverser/NodeTraverser.php | 1 + .../Validator/NodeTraverser/NodeTraverserInterface.php | 1 + .../{NodeTraverser => NodeVisitor}/AbstractVisitor.php | 2 +- .../{Validator => NodeVisitor}/NodeValidator.php | 3 +-- .../NodeVisitorInterface.php | 2 +- .../Validator/Tests/Validator/TraversingValidatorTest.php | 8 ++++---- .../Component/Validator/Validator/LegacyValidator.php | 5 +++++ 8 files changed, 15 insertions(+), 9 deletions(-) rename src/Symfony/Component/Validator/{NodeTraverser => NodeVisitor}/AbstractVisitor.php (92%) rename src/Symfony/Component/Validator/{Validator => NodeVisitor}/NodeValidator.php (97%) rename src/Symfony/Component/Validator/{NodeTraverser => NodeVisitor}/NodeVisitorInterface.php (91%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index c6b3da0601133..379b563ca2acd 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; +use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; use Symfony\Component\Validator\Validator\ValidatorInterface; /** diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index b1962c263aee8..036566aa44471 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\PropertyNode; +use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** * @since %%NextVersion%% diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 501c0ee7d1af4..d9ce42f025a8b 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** * @since %%NextVersion%% diff --git a/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php similarity index 92% rename from src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index c03e87c18da5d..31b49250eb8c9 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\NodeTraverser; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Node\Node; diff --git a/src/Symfony/Component/Validator/Validator/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php similarity index 97% rename from src/Symfony/Component/Validator/Validator/NodeValidator.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 7de32e88b13e8..c3b4fbe50223b 100644 --- a/src/Symfony/Component/Validator/Validator/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Validator; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; @@ -17,7 +17,6 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php similarity index 91% rename from src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index 0a70cc13fe7da..a7542d515a73f 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\NodeTraverser; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Node\Node; diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php index 2dc35387c799c..968fc7a79a642 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextManager; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Tests\AbstractValidatorTest; +use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Validator\NodeValidator; +use Symfony\Component\Validator\Tests\AbstractValidatorTest; use Symfony\Component\Validator\Validator\Validator; class TraversingValidatorTest extends AbstractValidatorTest diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 2227318289b8d..f8a2cc74c1bc8 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -24,4 +24,9 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals // TODO what about $traverse and $deep? return $this->validateObject($value, $groups); } + + public function getMetadataFactory() + { + return $this->metadataFactory; + } } From b1a947737ad219e5cba373ccf712bfa5287b599e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 15:15:09 +0100 Subject: [PATCH 015/323] [Validator] Added ObjectInitializer visitor --- .../Context/ExecutionContextManager.php | 10 +--- .../NodeVisitor/ObjectInitializer.php | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 379b563ca2acd..cfb93831f87d2 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -45,8 +45,7 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex public function __construct(GroupManagerInterface $groupManager) { $this->groupManager = $groupManager; - - $this->reset(); + $this->contextStack = new \SplStack(); } public function initialize(ValidatorInterface $validator) @@ -95,7 +94,7 @@ public function getCurrentContext() public function afterTraversal(array $nodes) { - $this->reset(); + $this->contextStack = new \SplStack(); } public function enterNode(Node $node) @@ -115,9 +114,4 @@ public function leaveNode(Node $node) $this->currentContext->popNode(); } - - private function reset() - { - $this->contextStack = new \SplStack(); - } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php new file mode 100644 index 0000000000000..617d10c84fd0d --- /dev/null +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeVisitor; + +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\ObjectInitializerInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ObjectInitializer extends AbstractVisitor +{ + /** + * @var ObjectInitializerInterface[] + */ + private $initializers; + + public function __construct(array $initializers) + { + foreach ($initializers as $initializer) { + if (!$initializer instanceof ObjectInitializerInterface) { + throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); + } + } + + $this->initializers = $initializers; + } + + public function enterNode(Node $node) + { + if ($node instanceof ClassNode) { + foreach ($this->initializers as $initializer) { + $initializer->initialize($node->value); + } + } + } +} From 1156bde82397c7029889eaf845e0158fe219dbd1 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 16:54:19 +0100 Subject: [PATCH 016/323] [Validator] Extracted code for group sequence resolving into GroupSequenceResolver --- .../Validator/NodeTraverser/NodeTraverser.php | 23 +------- .../NodeVisitor/GroupSequenceResolver.php | 49 ++++++++++++++++ .../Validator/NodeVisitor/NodeValidator.php | 58 ++++++++++++++----- .../NodeVisitor/ObjectInitializer.php | 10 ++-- 4 files changed, 101 insertions(+), 39 deletions(-) create mode 100644 src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 036566aa44471..159f7fdd5ad6c 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -116,27 +116,6 @@ private function traverseNode(Node $node) private function traverseClassNode(ClassNode $node) { - // Replace "Default" group by the group sequence attached to the class - // (if any) - foreach ($node->groups as $key => $group) { - if (Constraint::DEFAULT_GROUP !== $group) { - continue; - } - - if ($node->metadata->hasGroupSequence()) { - $node->groups[$key] = $node->metadata->getGroupSequence(); - } elseif ($node->metadata->isGroupSequenceProvider()) { - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $node->groups[$key] = $value->getGroupSequence(); - } - - // Cascade the "Default" group when validating the sequence - $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; - - // "Default" group found, abort - break; - } - $stopTraversal = false; foreach ($this->visitors as $visitor) { @@ -147,7 +126,7 @@ private function traverseClassNode(ClassNode $node) // Stop the traversal, but execute the leaveNode() methods anyway to // perform possible cleanups - if (!$stopTraversal) { + if (!$stopTraversal && count($node->groups) > 0) { foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { $this->traverseNode(new PropertyNode( diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php new file mode 100644 index 0000000000000..8b9741cecccb5 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeVisitor; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class GroupSequenceResolver extends AbstractVisitor +{ + public function enterNode(Node $node) + { + if (!$node instanceof ClassNode) { + return; + } + + if ($node->metadata->hasGroupSequence()) { + $groupSequence = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $value->getGroupSequence(); + } else { + return; + } + + $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); + + if (false !== $key) { + // Replace the "Default" group by the group sequence + $node->groups[$key] = $groupSequence; + + // Cascade the "Default" group when validating the sequence + $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } +} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index c3b4fbe50223b..12fad3cf0edfd 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** @@ -27,6 +28,8 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface { private $validatedObjects = array(); + private $validatedConstraints = array(); + /** * @var ConstraintValidatorFactoryInterface */ @@ -44,10 +47,15 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface private $currentGroup; + private $currentObjectHash; + + private $objectHashStack; + public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; + $this->objectHashStack = new \SplStack(); } public function initialize(ExecutionContextManagerInterface $contextManager) @@ -58,13 +66,20 @@ public function initialize(ExecutionContextManagerInterface $contextManager) public function afterTraversal(array $nodes) { $this->validatedObjects = array(); + $this->validatedConstraints = array(); + $this->objectHashStack = new \SplStack(); } public function enterNode(Node $node) { - $objectHash = $node instanceof ClassNode - ? spl_object_hash($node->value) - : null; + if ($node instanceof ClassNode) { + $objectHash = spl_object_hash($node->value); + $this->objectHashStack->push($objectHash); + } elseif ($node instanceof PropertyNode) { + $objectHash = $this->objectHashStack->top(); + } else { + $objectHash = null; + } // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort @@ -72,25 +87,27 @@ public function enterNode(Node $node) // finally call traverse() with remaining entries ([G3,G4]) or // simply continue traversal (if possible) - foreach ($node->groups as $group) { - // Validate object nodes only once per group - if (null !== $objectHash) { + foreach ($node->groups as $key => $group) { + // Remember which object was validated for which group + // Skip validation if the object was already validated for this + // group + if ($node instanceof ClassNode) { // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - // Exit, if the object is already validated for the current group if (isset($this->validatedObjects[$objectHash][$groupHash])) { - return false; + // Skip this group when validating properties + unset($node->groups[$key]); + + continue; } - // Remember validating this object before starting and possibly - // traversing the object graph $this->validatedObjects[$objectHash][$groupHash] = true; } - // Validate group sequence until a violation is generated + // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($node, $group); + $this->validateNodeForGroup($objectHash, $node, $group); continue; } @@ -100,6 +117,7 @@ public function enterNode(Node $node) continue; } + // Traverse group sequence until a violation is generated $this->traverseGroupSequence($node, $group); // Optimization: If the groups only contain the group sequence, @@ -139,17 +157,31 @@ private function traverseGroupSequence(ClassNode $node, GroupSequence $groupSequ } /** + * @param $objectHash * @param Node $node * @param $group * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group) + private function validateNodeForGroup($objectHash, Node $node, $group) { try { $this->currentGroup = $group; foreach ($node->metadata->findConstraints($group) as $constraint) { + // Remember the validated constraints of each object to prevent + // duplicate validation of constraints that belong to multiple + // validated groups + if (null !== $objectHash) { + $constraintHash = spl_object_hash($constraint); + + if (isset($this->validatedConstraints[$objectHash][$constraintHash])) { + continue; + } + + $this->validatedConstraints[$objectHash][$constraintHash] = true; + } + $validator = $this->validatorFactory->getInstance($constraint); $validator->initialize($this->contextManager->getCurrentContext()); $validator->validate($node->value, $constraint); diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php index 617d10c84fd0d..725c7b787eeca 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -39,10 +39,12 @@ public function __construct(array $initializers) public function enterNode(Node $node) { - if ($node instanceof ClassNode) { - foreach ($this->initializers as $initializer) { - $initializer->initialize($node->value); - } + if (!$node instanceof ClassNode) { + return; + } + + foreach ($this->initializers as $initializer) { + $initializer->initialize($node->value); } } } From 321d5bb30a37ec1807fb04d4093efe4fb7c908b9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 16:56:58 +0100 Subject: [PATCH 017/323] [Validator] Throw exception if ObjectInitializer is constructed without visitors --- .../Component/Validator/NodeVisitor/ObjectInitializer.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php index 725c7b787eeca..bd000366c518e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -30,10 +30,15 @@ public function __construct(array $initializers) { foreach ($initializers as $initializer) { if (!$initializer instanceof ObjectInitializerInterface) { - throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); + throw new \InvalidArgumentException('Validator initializers must implement ObjectInitializerInterface.'); } } + // If no initializer is present, this visitor should not even be created + if (0 === count($initializers)) { + throw new \InvalidArgumentException('Please pass at least one initializer.'); + } + $this->initializers = $initializers; } From 680f1ee6c71943195fb7ded4cce2e6628020e065 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 18:34:00 +0100 Subject: [PATCH 018/323] [Validator] Renamed $params to $parameters --- src/Symfony/Component/Validator/ExecutionContext.php | 8 ++++---- .../Component/Validator/ExecutionContextInterface.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 31a959187e354..00b8cca6369ee 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -115,14 +115,14 @@ public function addViolation($message, array $params = array(), $invalidValue = /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { $this->globalContext->getViolations()->add(new ConstraintViolation( null === $pluralization - ? $this->translator->trans($message, $params, $this->translationDomain) - : $this->translator->transChoice($message, $pluralization, $params, $this->translationDomain), + ? $this->translator->trans($message, $parameters, $this->translationDomain) + : $this->translator->transChoice($message, $pluralization, $parameters, $this->translationDomain), $message, - $params, + $parameters, $this->globalContext->getRoot(), $this->getPropertyPath($subPath), // check using func_num_args() to allow passing null values diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php index 0b6c86633d6e5..92f4c5690b0af 100644 --- a/src/Symfony/Component/Validator/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -104,14 +104,14 @@ public function addViolation($message, array $params = array(), $invalidValue = * * @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 array $parameters 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); + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null); /** * Validates the given value within the scope of the current validation. From 8ae68c9543c6f470bf9ec68b365ee4cb655ec8ee Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 13:03:34 +0100 Subject: [PATCH 019/323] [Validator] Made tests green (yay!) --- .../Validator/Context/ExecutionContext.php | 59 ++++++-- .../Context/ExecutionContextInterface.php | 4 +- .../Context/ExecutionContextManager.php | 31 +++- .../ExecutionContextManagerInterface.php | 4 +- .../Context/LegacyExecutionContext.php | 72 ++++++++- .../Validator/Mapping/CascadingStrategy.php | 27 ++++ .../Validator/Mapping/ClassMetadata.php | 19 ++- .../Validator/Mapping/ElementMetadata.php | 2 +- .../Validator/Mapping/MemberMetadata.php | 44 ++++-- .../Validator/Mapping/MetadataInterface.php | 4 +- .../Validator/Mapping/TraversalStrategy.php | 31 ++++ .../Validator/Mapping/ValueMetadata.php | 47 +++--- .../Component/Validator/Node/ClassNode.php | 5 +- src/Symfony/Component/Validator/Node/Node.php | 5 +- .../Component/Validator/Node/PropertyNode.php | 5 +- .../Validator/NodeTraverser/NodeTraverser.php | 110 ++++++++++++-- .../NodeVisitor/GroupSequenceResolver.php | 9 +- .../Validator/NodeVisitor/NodeValidator.php | 29 ++-- .../{ => Validator}/AbstractValidatorTest.php | 9 +- .../LegacyValidatorTest.php} | 19 ++- ...ingValidatorTest.php => ValidatorTest.php} | 10 +- .../Component/Validator/Util/PropertyPath.php | 36 +++++ .../Validator/Validator/AbstractValidator.php | 44 +++++- .../Validator/Validator/Validator.php | 8 +- .../Validator/ValidatorInterface.php | 2 + .../Violation/ConstraintViolationBuilder.php | 142 ++++++++++++++++++ .../ConstraintViolationBuilderInterface.php | 35 +++++ 27 files changed, 693 insertions(+), 119 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/CascadingStrategy.php create mode 100644 src/Symfony/Component/Validator/Mapping/TraversalStrategy.php rename src/Symfony/Component/Validator/Tests/{ => Validator}/AbstractValidatorTest.php (99%) rename src/Symfony/Component/Validator/Tests/{ValidatorTest.php => Validator/LegacyValidatorTest.php} (55%) rename src/Symfony/Component/Validator/Tests/Validator/{TraversingValidatorTest.php => ValidatorTest.php} (78%) create mode 100644 src/Symfony/Component/Validator/Util/PropertyPath.php create mode 100644 src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php create mode 100644 src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index ba4b0cb8018ff..80af8392c512d 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; /** * @since %%NextVersion%% @@ -48,18 +52,30 @@ class ExecutionContext implements ExecutionContextInterface */ private $groupManager; - public function __construct(ValidatorInterface $validator, GroupManagerInterface $groupManager) + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string + */ + private $translationDomain; + + public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { + $this->root = $root; $this->validator = $validator; $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); + $this->nodeStack = new \SplStack(); } public function pushNode(Node $node) { - if (null === $this->node) { - $this->root = $node->value; - } else { + if (null !== $this->node) { $this->nodeStack->push($this->node); } @@ -89,13 +105,32 @@ public function popNode() return $poppedNode; } - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $parameters = array()) { + $this->violations->add(new ConstraintViolation( + $this->translator->trans($message, $parameters, $this->translationDomain), + $message, + $parameters, + $this->root, + $this->getPropertyPath(), + $this->getValue(), + null, + null + )); } - public function buildViolation($message) + public function buildViolation($message, array $parameters = array()) { - + return new ConstraintViolationBuilder( + $this->violations, + $message, + $parameters, + $this->root, + $this->getPropertyPath(), + $this->getValue(), + $this->translator, + $this->translationDomain + ); } public function getViolations() @@ -141,15 +176,7 @@ public function getPropertyPath($subPath = '') { $propertyPath = $this->node ? $this->node->propertyPath : ''; - if (strlen($subPath) > 0) { - if ('[' === $subPath{1}) { - return $propertyPath.$subPath; - } - - return $propertyPath ? $propertyPath.'.'.$subPath : $subPath; - } - - return $propertyPath; + return PropertyPath::append($propertyPath, $subPath); } /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 4a8765f27be5c..b6eeb73d353b2 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -30,11 +30,11 @@ public function getValidator(); * 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 array $parameters The parameters substituted in the error message. * * @api */ - public function addViolation($message, array $params = array()); + public function addViolation($message, array $parameters = array()); public function buildViolation($message); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index cfb93831f87d2..369b6a062b265 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; @@ -42,9 +43,21 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex */ private $contextStack; - public function __construct(GroupManagerInterface $groupManager) + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string|null + */ + private $translationDomain; + + public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; $this->contextStack = new \SplStack(); } @@ -53,13 +66,23 @@ public function initialize(ValidatorInterface $validator) $this->validator = $validator; } - public function startContext() + public function startContext($root) { + if (null === $this->validator) { + // TODO error, call initialize() first + } + if (null !== $this->currentContext) { $this->contextStack->push($this->currentContext); } - $this->currentContext = new ExecutionContext($this->validator, $this->groupManager); + $this->currentContext = new LegacyExecutionContext( + $root, + $this->validator, + $this->groupManager, + $this->translator, + $this->translationDomain + ); return $this->currentContext; } @@ -100,7 +123,7 @@ public function afterTraversal(array $nodes) public function enterNode(Node $node) { if (null === $this->currentContext) { - // error no context started + // TODO error call startContext() first } $this->currentContext->pushNode($node); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php index 0d79eb43bb3de..b805c12e43364 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -18,9 +18,11 @@ interface ExecutionContextManagerInterface { /** + * @param mixed $root + * * @return ExecutionContextInterface The started context */ - public function startContext(); + public function startContext($root); /** * @return ExecutionContextInterface The stopped context diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 1981e0f00ed47..8cf17f5ee3ec5 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -11,7 +11,12 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** * @since %%NextVersion%% @@ -19,23 +24,84 @@ */ class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface { - public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { + if (!$validator instanceof LegacyValidatorInterface) { + throw new InvalidArgumentException( + 'The validator passed to LegacyExecutionContext must implement '. + '"Symfony\Component\Validator\ValidatorInterface".' + ); + } + parent::__construct($root, $validator, $groupManager, $translator, $translationDomain); + } + + /** + * {@inheritdoc} + */ + public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + { + if (func_num_args() >= 3) { + $this + ->buildViolation($message, $parameters) + ->setInvalidValue($invalidValue) + ->setPluralization($pluralization) + ->setCode($code) + ->addViolation() + ; + + return; + } + + parent::addViolation($message, $parameters); + } + + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + { + if (func_num_args() >= 3) { + $this + ->buildViolation($message, $parameters) + ->atPath($subPath) + ->setInvalidValue($invalidValue) + ->setPluralization($pluralization) + ->setCode($code) + ->addViolation() + ; + + return; + } + + $this + ->buildViolation($message, $parameters) + ->atPath($subPath) + ->addViolation() + ; } public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { + // TODO handle $traverse and $deep + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateObject($value, $groups) + ; } public function validateValue($value, $constraints, $subPath = '', $groups = null) { - + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateValue($value, $constraints, $groups) + ; } public function getMetadataFactory() { - + return $this->getValidator()->getMetadataFactory(); } } diff --git a/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php new file mode 100644 index 0000000000000..1218c2d484c38 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.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\Mapping; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class CascadingStrategy +{ + const NONE = 0; + + const CASCADE = 1; + + private function __construct() + { + } +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 8bba73a01f13e..16c42ccd0826e 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -26,7 +26,7 @@ * @author Bernhard Schussek * @author Fabien Potencier */ -class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface +class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface, ClassMetadataInterface { /** * @var string @@ -63,6 +63,8 @@ class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassB */ public $groupSequenceProvider = false; + public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** * @var \ReflectionClass */ @@ -423,4 +425,19 @@ public function isGroupSequenceProvider() { return $this->groupSequenceProvider; } + + /** + * Class nodes are never cascaded. + * + * @return Boolean Always returns false. + */ + public function getCascadingStrategy() + { + return CascadingStrategy::NONE; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } } diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index 9dedb79fd95e4..cfe34e3985f5b 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraint; -abstract class ElementMetadata +abstract class ElementMetadata implements MetadataInterface { /** * @var Constraint[] diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index c30a87ee06269..f3b3e3cf8fed2 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -12,20 +12,18 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ValidationVisitorInterface; -use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\PropertyMetadataInterface; +use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, ClassBasedInterface +abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, LegacyPropertyMetadataInterface { public $class; public $name; public $property; - public $cascaded = false; - public $collectionCascaded = false; - public $collectionCascadedDeeply = false; + public $cascadingStrategy = CascadingStrategy::NONE; + public $traversalStrategy = TraversalStrategy::IMPLICIT; private $reflMember = array(); /** @@ -64,10 +62,15 @@ public function addConstraint(Constraint $constraint) } if ($constraint instanceof Valid) { - $this->cascaded = true; - /* @var Valid $constraint */ - $this->collectionCascaded = $constraint->traverse; - $this->collectionCascadedDeeply = $constraint->deep; + $this->cascadingStrategy = CascadingStrategy::CASCADE; + + if ($constraint->traverse) { + $this->traversalStrategy = TraversalStrategy::TRAVERSE; + } + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } } else { parent::addConstraint($constraint); } @@ -86,9 +89,8 @@ public function __sleep() 'class', 'name', 'property', - 'cascaded', - 'collectionCascaded', - 'collectionCascadedDeeply', + 'cascadingStrategy', + 'traversalStrategy', )); } @@ -158,6 +160,16 @@ public function isPrivate($objectOrClassName) return $this->getReflectionMember($objectOrClassName)->isPrivate(); } + public function getCascadingStrategy() + { + return $this->cascadingStrategy; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } + /** * Returns whether objects stored in this member should be validated * @@ -165,7 +177,7 @@ public function isPrivate($objectOrClassName) */ public function isCascaded() { - return $this->cascaded; + return $this->cascadingStrategy & CascadingStrategy::CASCADE; } /** @@ -176,7 +188,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return $this->collectionCascaded; + return $this->traversalStrategy & TraversalStrategy::TRAVERSE; } /** @@ -187,7 +199,7 @@ public function isCollectionCascaded() */ public function isCollectionCascadedDeeply() { - return $this->collectionCascadedDeeply; + return $this->traversalStrategy & TraversalStrategy::RECURSIVE; } /** diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index 3df0d9bc0d587..540c4c9d6064c 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -53,5 +53,7 @@ interface MetadataInterface */ public function findConstraints($group); - public function supportsCascading(); + public function getCascadingStrategy(); + + public function getTraversalStrategy(); } diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php new file mode 100644 index 0000000000000..4a9d8c8aa1551 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class TraversalStrategy +{ + const IMPLICIT = 0; + + const NONE = 1; + + const TRAVERSE = 2; + + const RECURSIVE = 4; + + private function __construct() + { + } +} diff --git a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php index c51a6fa575dae..230e5dd947fe6 100644 --- a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php @@ -11,35 +11,48 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\ValidatorException; + /** * @since %%NextVersion%% * @author Bernhard Schussek */ -class ValueMetadata implements MetadataInterface +class ValueMetadata extends ElementMetadata { - /** - * Returns all constraints for a given validation group. - * - * @param string $group The validation group. - * - * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. - */ - public function findConstraints($group) - { - - } - - public function supportsCascading() + public function __construct(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) + )); + } + + $this->addConstraint($constraint); + } } - public function supportsIteration() + public function getCascadingStrategy() { } - public function supportsRecursiveIteration() + public function getTraversalStrategy() { } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index dfb06dbc0325e..d49bf81c77b80 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -24,7 +24,7 @@ class ClassNode extends Node */ public $metadata; - public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups) + public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { if (!is_object($value)) { // error @@ -34,7 +34,8 @@ public function __construct($value, ClassMetadataInterface $metadata, $propertyP $value, $metadata, $propertyPath, - $groups + $groups, + $cascadedGroups ); } diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 3dead5623d8ac..08b2e4da7835a 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -27,11 +27,14 @@ abstract class Node public $groups; - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups) + public $cascadedGroups; + + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { $this->value = $value; $this->metadata = $metadata; $this->propertyPath = $propertyPath; $this->groups = $groups; + $this->cascadedGroups = $cascadedGroups; } } diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 9424acb59f526..76cfcb35312ee 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -24,13 +24,14 @@ class PropertyNode extends Node */ public $metadata; - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups) + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { parent::__construct( $value, $metadata, $propertyPath, - $groups + $groups, + $cascadedGroups ); } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 159f7fdd5ad6c..ce1c6029b5aee 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -98,15 +101,25 @@ private function traverseNode(Node $node) // Stop the traversal, but execute the leaveNode() methods anyway to // perform possible cleanups - if (!$stopTraversal && is_object($node->value) && $node->metadata->supportsCascading()) { - $classMetadata = $this->metadataFactory->getMetadataFor($node->value); - - $this->traverseClassNode(new ClassNode( - $node->value, - $classMetadata, - $node->propertyPath, - $node->groups - )); + if (!$stopTraversal && null !== $node->value) { + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->metadata->getTraversalStrategy(); + + if (is_array($node->value)) { + $this->cascadeCollection( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + } elseif ($cascadingStrategy & CascadingStrategy::CASCADE) { + $this->cascadeObject( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + } } foreach ($this->visitors as $visitor) { @@ -114,7 +127,7 @@ private function traverseNode(Node $node) } } - private function traverseClassNode(ClassNode $node) + private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) { $stopTraversal = false; @@ -135,14 +148,89 @@ private function traverseClassNode(ClassNode $node) $node->propertyPath ? $node->propertyPath.'.'.$propertyName : $propertyName, - $node->groups + $node->groups, + $node->cascadedGroups )); } } + + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + $traversalStrategy = $node->metadata->getTraversalStrategy(); + } + + if ($traversalStrategy & TraversalStrategy::TRAVERSE) { + $this->cascadeCollection( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + } } foreach ($this->visitors as $visitor) { $visitor->leaveNode($node); } } + + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + $classNode = new ClassNode( + $object, + $classMetadata, + $propertyPath, + $groups, + $groups + ); + + $this->traverseClassNode($classNode, $traversalStrategy); + } catch (NoSuchMetadataException $e) { + if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) { + throw $e; + } + + // Metadata doesn't necessarily have to exist for + // traversable objects, because we know how to validate + // them anyway. + $this->cascadeCollection( + $object, + $propertyPath, + $groups, + $traversalStrategy + ); + } + } + + private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy) + { + if (!($traversalStrategy & TraversalStrategy::RECURSIVE)) { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } + + foreach ($collection as $key => $value) { + if (is_array($value)) { + $this->cascadeCollection( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy + ); + + continue; + } + + // Scalar and null values in the collection are ignored + if (is_object($value)) { + $this->cascadeObject( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy + ); + } + } + } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php index 8b9741cecccb5..047f2ad60e496 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -31,7 +32,11 @@ public function enterNode(Node $node) $groupSequence = $node->metadata->getGroupSequence(); } elseif ($node->metadata->isGroupSequenceProvider()) { /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $value->getGroupSequence(); + $groupSequence = $node->value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } } else { return; } @@ -43,7 +48,7 @@ public function enterNode(Node $node) $node->groups[$key] = $groupSequence; // Cascade the "Default" group when validating the sequence - $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; } } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 12fad3cf0edfd..662262a4a5bf1 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -75,7 +75,7 @@ public function enterNode(Node $node) if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); $this->objectHashStack->push($objectHash); - } elseif ($node instanceof PropertyNode) { + } elseif ($node instanceof PropertyNode && count($this->objectHashStack) > 0) { $objectHash = $this->objectHashStack->top(); } else { $objectHash = null; @@ -112,10 +112,8 @@ public function enterNode(Node $node) continue; } - // Only traverse group sequences at class, not at property level - if (!$node instanceof ClassNode) { - continue; - } + // Skip the group sequence when validating properties + unset($node->groups[$key]); // Traverse group sequence until a violation is generated $this->traverseGroupSequence($node, $group); @@ -130,24 +128,29 @@ public function enterNode(Node $node) return true; } + public function leaveNode(Node $node) + { + if ($node instanceof ClassNode) { + $this->objectHashStack->pop(); + } + } + public function getCurrentGroup() { return $this->currentGroup; } - private function traverseGroupSequence(ClassNode $node, GroupSequence $groupSequence) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) { $context = $this->contextManager->getCurrentContext(); $violationCount = count($context->getViolations()); foreach ($groupSequence->groups as $groupInSequence) { - $this->nodeTraverser->traverse(array(new ClassNode( - $node->value, - $node->metadata, - $node->propertyPath, - array($groupInSequence), - array($groupSequence->cascadedGroup ?: $groupInSequence) - ))); + $node = clone $node; + $node->groups = array($groupInSequence); + $node->cascadedGroups = array($groupSequence->cascadedGroup ?: $groupInSequence); + + $this->nodeTraverser->traverse(array($node)); // Abort sequence validation if a violation was generated if (count($context->getViolations()) > $violationCount) { diff --git a/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php similarity index 99% rename from src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php rename to src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 33e39c5345e04..c3148813202d8 100644 --- a/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Tests; +namespace Symfony\Component\Validator\Tests\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; @@ -788,7 +788,6 @@ public function testValidateValue() $test->assertNull($context->getPropertyName()); $test->assertSame('', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); - $test->assertNull($context->getMetadata()); $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame('Bernhard', $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); @@ -942,8 +941,6 @@ public function testValidateMultipleGroups() public function testNoDuplicateValidationIfConstraintInMultipleGroups() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $callback = function ($value, ExecutionContextInterface $context) { @@ -963,8 +960,6 @@ public function testNoDuplicateValidationIfConstraintInMultipleGroups() public function testGroupSequenceAbortsAfterFailedGroup() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $callback1 = function ($value, ExecutionContextInterface $context) { @@ -997,8 +992,6 @@ public function testGroupSequenceAbortsAfterFailedGroup() public function testGroupSequenceIncludesReferences() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $entity->reference = new Reference(); diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php similarity index 55% rename from src/Symfony/Component/Validator/Tests/ValidatorTest.php rename to src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index 52bdbea519af2..327194d751392 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -9,17 +9,32 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Tests; +namespace Symfony\Component\Validator\Tests\Validator; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -class ValidatorTest extends AbstractValidatorTest +class LegacyValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { return new Validator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); } + + public function testNoDuplicateValidationIfConstraintInMultipleGroups() + { + $this->markTestSkipped('Currently not supported'); + } + + public function testGroupSequenceAbortsAfterFailedGroup() + { + $this->markTestSkipped('Currently not supported'); + } + + public function testGroupSequenceIncludesReferences() + { + $this->markTestSkipped('Currently not supported'); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php similarity index 78% rename from src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php rename to src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php index 968fc7a79a642..f990f10e04c3c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php @@ -15,19 +15,20 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextManager; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\Tests\AbstractValidatorTest; -use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\LegacyValidator; -class TraversingValidatorTest extends AbstractValidatorTest +class ValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); // The context manager needs the validator for passing it to created // contexts @@ -37,6 +38,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) // context to the constraint validators $nodeValidator->initialize($contextManager); + $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextManager); $nodeTraverser->addVisitor($nodeValidator); diff --git a/src/Symfony/Component/Validator/Util/PropertyPath.php b/src/Symfony/Component/Validator/Util/PropertyPath.php new file mode 100644 index 0000000000000..bf33b50b5e4a6 --- /dev/null +++ b/src/Symfony/Component/Validator/Util/PropertyPath.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Util; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class PropertyPath +{ + public static function append($basePath, $subPath) + { + if ('' !== (string) $subPath) { + if ('[' === $subPath{1}) { + return $basePath.$subPath; + } + + return $basePath ? $basePath.'.'.$subPath : $subPath; + } + + return $basePath; + } + + private function __construct() + { + } +} diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index 5401fc6dbe21d..69f48ca7d0e61 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -13,6 +13,8 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\ValueMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; @@ -20,6 +22,7 @@ use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Node\ValueNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\Util\PropertyPath; /** * @since %%NextVersion%% @@ -75,15 +78,22 @@ protected function traverseObject($object, $groups = null) $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); } + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $this->nodeTraverser->traverse(array(new ClassNode( $object, $classMetadata, $this->defaultPropertyPath, - // TODO use cascade group here - $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + $groups, + $groups ))); } @@ -92,7 +102,12 @@ protected function traverseProperty($object, $propertyName, $groups = null) $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); } $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); @@ -105,7 +120,8 @@ protected function traverseProperty($object, $propertyName, $groups = null) $nodes[] = new PropertyNode( $propertyValue, $propertyMetadata, - $this->defaultPropertyPath, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, $groups ); } @@ -118,7 +134,12 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); } $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); @@ -129,7 +150,8 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups $nodes[] = new PropertyNode( $value, $propertyMetadata, - $this->defaultPropertyPath, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, $groups ); } @@ -139,13 +161,19 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups protected function traverseValue($value, $constraints, $groups = null) { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + $metadata = new ValueMetadata($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $this->nodeTraverser->traverse(array(new ValueNode( $value, $metadata, $this->defaultPropertyPath, - $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + $groups, + $groups ))); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index ed222d57c81dc..0c0c38880b33d 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -35,7 +35,7 @@ public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFacto public function validateObject($object, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traverseObject($object, $groups); @@ -44,7 +44,7 @@ public function validateObject($object, $groups = null) public function validateProperty($object, $propertyName, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traverseProperty($object, $propertyName, $groups); @@ -53,7 +53,7 @@ public function validateProperty($object, $propertyName, $groups = null) public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traversePropertyValue($object, $propertyName, $value, $groups); @@ -62,7 +62,7 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = public function validateValue($value, $constraints, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($value); $this->traverseValue($value, $constraints, $groups); diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index f02ed79d9b756..7985a1f9ef4e8 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -35,6 +35,8 @@ interface ValidatorInterface */ public function validateObject($object, $groups = null); +// public function validateCollection($collection, $groups = null); + /** * Validates a property of a value against its current value. * diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php new file mode 100644 index 0000000000000..5fb8488e80fcf --- /dev/null +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Violation; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Util\PropertyPath; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface +{ + private $violations; + + private $message; + + private $parameters; + + private $root; + + private $invalidValue; + + private $propertyPath; + + private $translator; + + private $translationDomain; + + private $pluralization; + + private $code; + + public function __construct(ConstraintViolationList $violations, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = null) + { + $this->violations = $violations; + $this->message = $message; + $this->parameters = $parameters; + $this->root = $root; + $this->propertyPath = $propertyPath; + $this->invalidValue = $invalidValue; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + public function atPath($subPath) + { + $this->propertyPath = PropertyPath::append($this->propertyPath, $subPath); + + return $this; + } + + public function setParameter($key, $value) + { + $this->parameters[$key] = $value; + + return $this; + } + + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + + return $this; + } + + public function setTranslationDomain($translationDomain) + { + $this->translationDomain = $translationDomain; + + return $this; + } + + public function setInvalidValue($invalidValue) + { + $this->invalidValue = $invalidValue; + + return $this; + } + + public function setPluralization($pluralization) + { + $this->pluralization = $pluralization; + + return $this; + } + + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + public function addViolation() + { + if (null === $this->pluralization) { + $translatedMessage = $this->translator->trans( + $this->message, + $this->parameters, + $this->translationDomain + ); + } else { + try { + $translatedMessage = $this->translator->transChoice( + $this->message, + $this->pluralization, + $this->parameters, + $this->translationDomain# + ); + } catch (\InvalidArgumentException $e) { + $translatedMessage = $this->translator->trans( + $this->message, + $this->parameters, + $this->translationDomain + ); + } + } + + $this->violations->add(new ConstraintViolation( + $translatedMessage, + $this->message, + $this->parameters, + $this->root, + $this->propertyPath, + $this->invalidValue, + $this->pluralization, + $this->code + )); + } +} diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php new file mode 100644 index 0000000000000..9d62c3ccb5dce --- /dev/null +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Violation; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ConstraintViolationBuilderInterface +{ + public function atPath($subPath); + + public function setParameter($key, $value); + + public function setParameters(array $parameters); + + public function setTranslationDomain($translationDomain); + + public function setInvalidValue($invalidValue); + + public function setPluralization($pluralization); + + public function setCode($code); + + public function addViolation(); +} From c1b1e0339990a0ba14ce9b801a8d558dec75ab4a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 13:17:23 +0100 Subject: [PATCH 020/323] [Validator] Added TODO reminder --- .../Component/Validator/NodeVisitor/GroupSequenceResolver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php index 047f2ad60e496..868d6fc428749 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -34,6 +34,7 @@ public function enterNode(Node $node) /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ $groupSequence = $node->value->getGroupSequence(); + // TODO test if (!$groupSequence instanceof GroupSequence) { $groupSequence = new GroupSequence($groupSequence); } From 5fbf848f2ff35ae413570ce1ed462b1e81bbadde Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 15:23:59 +0100 Subject: [PATCH 021/323] [Validator] Added note about Callback constraint to CHANGELOG --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index ecdf0cf366a6e..005526215beae 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added `DoctrineCache` to adapt any Doctrine cache * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` * changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array + * `Callback` can now be put onto properties (useful when you pass a closure to the constraint) 2.4.0 ----- From 8318286650b6ac220b64ffec2ac1ea67fff1d0f9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 15:42:46 +0100 Subject: [PATCH 022/323] [Validator] Completed GroupSequence implementation --- .../Validator/Constraints/GroupSequence.php | 112 +++++++++++++++++- .../Exception/OutOfBoundsException.php | 21 ++++ .../Tests/Constraints/GroupSequenceTest.php | 18 +++ 3 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Validator/Exception/OutOfBoundsException.php diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 7985b6cc9726b..ef93b9bc77139 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -11,10 +11,44 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Exception\OutOfBoundsException; use Traversable; /** - * Annotation for group sequences + * A sequence of validation groups. + * + * When validating a group sequence, each group will only be validated if all + * of the previous groups in the sequence succeeded. For example: + * + * $validator->validateObject($address, new GroupSequence('Basic', 'Strict')); + * + * In the first step, all constraints that belong to the group "Basic" will be + * validated. If none of the constraints fail, the validator will then validate + * the constraints in group "Strict". This is useful, for example, if "Strict" + * contains expensive checks that require a lot of CPU or slow, external + * services. You usually don't want to run expensive checks if any of the cheap + * checks fails. + * + * When adding metadata to a class, you can override the "Default" group of + * that class with a group sequence: + * + * /** + * * @GroupSequence({"Address", "Strict"}) + * *\/ + * class Address + * { + * // ... + * } + * + * Whenever you validate that object in the "Default" group, the group sequence + * will be validated: + * + * $validator->validateObject($address); + * + * If you want to execute the constraints of the "Default" group for a class + * with an overridden default group, pass the class name as group name instead: + * + * $validator->validateObject($address, "Address") * * @Annotation * @@ -25,13 +59,14 @@ class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable { /** - * The members of the sequence - * @var array + * The groups in the sequence + * + * @var string[]|GroupSequence[] */ public $groups; /** - * The group under which cascaded objects are validated when validating + * The group in which cascaded objects are validated when validating * this sequence. * * By default, cascaded objects are validated in each of the groups of @@ -46,27 +81,80 @@ class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable */ public $cascadedGroup; + /** + * Creates a new group sequence. + * + * @param string[] $groups The groups in the sequence + */ public function __construct(array $groups) { // Support for Doctrine annotations $this->groups = isset($groups['value']) ? $groups['value'] : $groups; } + /** + * Returns an iterator for this group. + * + * @return Traversable The iterator + * + * @see \IteratorAggregate::getIterator() + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function getIterator() { return new \ArrayIterator($this->groups); } + /** + * Returns whether the given offset exists in the sequence. + * + * @param integer $offset The offset + * + * @return Boolean Whether the offset exists + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetExists($offset) { return isset($this->groups[$offset]); } + /** + * Returns the group at the given offset. + * + * @param integer $offset The offset + * + * @return string The group a the given offset + * + * @throws OutOfBoundsException If the object does not exist + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetGet($offset) { + if (!isset($this->groups[$offset])) { + throw new OutOfBoundsException(sprintf( + 'The offset "%s" does not exist.', + $offset + )); + } + return $this->groups[$offset]; } + /** + * Sets the group at the given offset. + * + * @param integer $offset The offset + * @param string $value The group name + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetSet($offset, $value) { if (null !== $offset) { @@ -78,11 +166,27 @@ public function offsetSet($offset, $value) $this->groups[] = $value; } + /** + * Removes the group at the given offset. + * + * @param integer $offset The offset + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetUnset($offset) { unset($this->groups[$offset]); } + /** + * Returns the number of groups in the sequence. + * + * @return integer The number of groups + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function count() { return count($this->groups); diff --git a/src/Symfony/Component/Validator/Exception/OutOfBoundsException.php b/src/Symfony/Component/Validator/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000000..30906e8a82ca0 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/OutOfBoundsException.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 OutOfBoundsException for the Validator component. + * + * @author Bernhard Schussek + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php index 83275d1c72733..85b60b5eee3d5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php @@ -63,4 +63,22 @@ public function testArrayAccess() $this->assertTrue(isset($sequence[0])); $this->assertSame('Group 1', $sequence[0]); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\OutOfBoundsException + */ + public function testGetExpectsExistingKey() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $sequence[2]; + } + + public function testUnsetIgnoresNonExistingKeys() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + // should not fail + unset($sequence[2]); + } } From f6b72884493591f8a0d75c90d35b353d00a14c13 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 15:44:01 +0100 Subject: [PATCH 023/323] [Validator] Removed unused use statement --- src/Symfony/Component/Validator/Constraints/GroupSequence.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index ef93b9bc77139..e2fe768c35648 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Exception\OutOfBoundsException; -use Traversable; /** * A sequence of validation groups. From 4ea3ff6688988faab3f31b8a6af1fc5a286af896 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:18:15 +0100 Subject: [PATCH 024/323] [Validator] Finished inline documentation of ExecutionContext[Interface] --- .../Validator/Context/ExecutionContext.php | 98 +++++++++--- .../Context/ExecutionContextInterface.php | 145 +++++++++++++----- .../{ValueMetadata.php => AdHocMetadata.php} | 2 +- .../Validator/Validator/AbstractValidator.php | 4 +- 4 files changed, 190 insertions(+), 59 deletions(-) rename src/Symfony/Component/Validator/Mapping/{ValueMetadata.php => AdHocMetadata.php} (97%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 80af8392c512d..bc6abc39b1d53 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -23,21 +23,39 @@ use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; /** - * @since %%NextVersion%% + * The context used and created by {@link ExecutionContextManager}. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see ExecutionContextInterface */ class ExecutionContext implements ExecutionContextInterface { + /** + * The root value of the validated object graph. + * + * @var mixed + */ private $root; + /** + * The violations generated in the current context. + * + * @var ConstraintViolationList + */ private $violations; /** + * The current node under validation. + * * @var Node */ private $node; /** + * The trace of nodes from the root node to the current node. + * * @var \SplStack */ private $nodeStack; @@ -73,25 +91,39 @@ public function __construct($root, ValidatorInterface $validator, GroupManagerIn $this->nodeStack = new \SplStack(); } + /** + * Sets the values of the context to match the given node. + * + * Internally, all nodes are stored on a stack and can be removed from that + * stack using {@link popNode()}. + * + * @param Node $node The currently validated node + */ public function pushNode(Node $node) { - if (null !== $this->node) { - $this->nodeStack->push($this->node); - } - + $this->nodeStack->push($node); $this->node = $node; } + /** + * Sets the values of the context to match the previous node. + * + * The current node is removed from the internal stack and returned. + * + * @return Node|null The currently validated node or null, if no node was + * on the stack + */ public function popNode() { - $poppedNode = $this->node; - + // Nothing to do if the stack is empty if (0 === count($this->nodeStack)) { - $this->node = null; - - return $poppedNode; + return null; } + $poppedNode = $this->node; + + // After removing the last node, the stack is empty and the node + // is null if (1 === count($this->nodeStack)) { $this->nodeStack->pop(); $this->node = null; @@ -105,6 +137,9 @@ public function popNode() return $poppedNode; } + /** + * {@inheritdoc} + */ public function addViolation($message, array $parameters = array()) { $this->violations->add(new ConstraintViolation( @@ -119,6 +154,9 @@ public function addViolation($message, array $parameters = array()) )); } + /** + * {@inheritdoc} + */ public function buildViolation($message, array $parameters = array()) { return new ConstraintViolationBuilder( @@ -133,31 +171,57 @@ public function buildViolation($message, array $parameters = array()) ); } + /** + * {@inheritdoc} + */ public function getViolations() { return $this->violations; } + /** + * {@inheritdoc} + */ + public function getValidator() + { + return $this->validator; + } + + /** + * {@inheritdoc} + */ public function getRoot() { return $this->root; } + /** + * {@inheritdoc} + */ public function getValue() { return $this->node ? $this->node->value : null; } + /** + * {@inheritdoc} + */ public function getMetadata() { return $this->node ? $this->node->metadata : null; } + /** + * {@inheritdoc} + */ public function getGroup() { return $this->groupManager->getCurrentGroup(); } + /** + * {@inheritdoc} + */ public function getClassName() { $metadata = $this->getMetadata(); @@ -165,6 +229,9 @@ public function getClassName() return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null; } + /** + * {@inheritdoc} + */ public function getPropertyName() { $metadata = $this->getMetadata(); @@ -172,18 +239,13 @@ public function getPropertyName() return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null; } + /** + * {@inheritdoc} + */ public function getPropertyPath($subPath = '') { $propertyPath = $this->node ? $this->node->propertyPath : ''; return PropertyPath::append($propertyPath, $subPath); } - - /** - * @return ValidatorInterface - */ - public function getValidator() - { - return $this->validator; - } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index b6eeb73d353b2..5ff0a817243f7 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -11,52 +11,121 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; /** - * @since %%NextVersion%% + * The context of a validation run. + * + * The context collects all violations generated during the validation. By + * default, validators execute all validations in a new context: + * + * $violations = $validator->validateObject($object); + * + * When you make another call to the validator, while the validation is in + * progress, the violations will be isolated from each other: + * + * public function validate($value, Constraint $constraint) + * { + * $validator = $this->context->getValidator(); + * + * // The violations are not added to $this->context + * $violations = $validator->validateObject($value); + * } + * + * However, if you want to add the violations to the current context, use the + * {@link ValidatorInterface::inContext()} method: + * + * public function validate($value, Constraint $constraint) + * { + * $validator = $this->context->getValidator(); + * + * // The violations are added to $this->context + * $validator + * ->inContext($this->context) + * ->validateObject($value) + * ; + * } + * + * Additionally, the context provides information about the current state of + * the validator, such as the currently validated class, the name of the + * currently validated property and more. These values change over time, so you + * cannot store a context and expect that the methods still return the same + * results later on. + * + * @since 2.5 * @author Bernhard Schussek */ interface ExecutionContextInterface { /** - * @return ValidatorInterface + * Adds a violation at the current node of the validation graph. + * + * @param string $message The error message + * @param array $parameters The parameters substituted in the error message */ - public function getValidator(); + public function addViolation($message, array $parameters = array()); /** - * Adds a violation at the current node of the validation graph. + * Returns a builder for adding a violation with extended information. + * + * Call {@link ConstraintViolationBuilderInterface::addViolation()} to + * add the violation when you're done with the configuration: * - * @param string $message The error message. - * @param array $parameters The parameters substituted in the error message. + * $context->buildViolation('Please enter a number between %min% and %max.') + * ->setParameter('%min%', 3) + * ->setParameter('%max%', 10) + * ->setTranslationDomain('number_validation') + * ->addViolation(); * - * @api + * @param string $message The error message + * @param array $parameters The parameters substituted in the error message + * + * @return ConstraintViolationBuilderInterface The violation builder */ - public function addViolation($message, array $parameters = array()); + public function buildViolation($message, array $parameters = array()); - public function buildViolation($message); + /** + * Returns the violations generated in this context. + * + * @return ConstraintViolationListInterface The constraint violations + */ + public function getViolations(); /** - * Returns the violations generated by the validator so far. + * Returns the validator. * - * @return ConstraintViolationListInterface The constraint violation list. + * Useful if you want to validate additional constraints: * - * @api + * public function validate($value, Constraint $constraint) + * { + * $validator = $this->context->getValidator(); + * + * $violations = $validator->validateValue($value, new Length(array('min' => 3))); + * + * if (count($violations) > 0) { + * // ... + * } + * } + * + * @return ValidatorInterface */ - public function getViolations(); + public function getValidator(); /** - * Returns the value at which validation was started in the object graph. + * Returns the root value of 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. + * object at which the traversal started. * - * The current value is returned by {@link getValue}. + * The current value is returned by {@link getValue()}. * - * @return mixed The root value of the validation. + * @return mixed|null The root value of the validation or null, if no value + * is currently being validated */ public function getRoot(); @@ -64,9 +133,10 @@ 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}. + * validator, use {@link getRoot()}. * - * @return mixed The currently validated value. + * @return mixed|null The currently validated value or null, if no value is + * currently being validated */ public function getValue(); @@ -77,21 +147,22 @@ public function getValue(); * {@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. + * the validated value is the result of a getter method. The metadata can + * also be an {@link Mapping\AdHocMetadata} if the current value does not + * belong to any structural element. * * @return MetadataInterface|null The metadata of the currently validated - * value. + * value or null, if no value is currently + * being validated */ public function getMetadata(); /** * Returns the validation group that is currently being validated. * - * @return string The current validation group. + * @return string|GroupSequence|null The current validation group or null, + * if no value is currently being + * validated */ public function getGroup(); @@ -99,10 +170,10 @@ 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. + * {@link ClassBasedInterface}, this method returns null. * - * @return string|null The class name or null, if no class name could be found. + * @return string|null The class name or null, if no class name could be + * found */ public function getClassName(); @@ -110,10 +181,10 @@ 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. + * {@link PropertyMetadataInterface}, this method returns null. * - * @return string|null The property name or null, if no property name could be found. + * @return string|null The property name or null, if no property name could + * be found */ public function getPropertyName(); @@ -123,9 +194,7 @@ public function getPropertyName(); * * For example, take the following object graph: * - *
-     * (Person)---($address: Address)---($street: string)
-     * 
+ * (Person)---($address: Address)---($street: string) * * When the Person instance is passed to the validator, the * property path is initially empty. When the $address property @@ -137,16 +206,16 @@ public function getPropertyName(); * 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 + * of Address instances, 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. + * 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. + * root value of the validation graph */ public function getPropertyPath($subPath = ''); } diff --git a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php similarity index 97% rename from src/Symfony/Component/Validator/Mapping/ValueMetadata.php rename to src/Symfony/Component/Validator/Mapping/AdHocMetadata.php index 230e5dd947fe6..a7b836d3415ba 100644 --- a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php @@ -18,7 +18,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ValueMetadata extends ElementMetadata +class AdHocMetadata extends ElementMetadata { public function __construct(array $constraints) { diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index 69f48ca7d0e61..c57473e751d89 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\ValueMetadata; +use Symfony\Component\Validator\Mapping\AdHocMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\PropertyNode; @@ -165,7 +165,7 @@ protected function traverseValue($value, $constraints, $groups = null) $constraints = array($constraints); } - $metadata = new ValueMetadata($constraints); + $metadata = new AdHocMetadata($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $this->nodeTraverser->traverse(array(new ValueNode( From adc1437fab6eb6e4ec30f44059db49f9483b70fc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:23:52 +0100 Subject: [PATCH 025/323] [Validator] Fixed failing tests --- src/Symfony/Component/Validator/Mapping/AdHocMetadata.php | 2 +- src/Symfony/Component/Validator/Mapping/ClassMetadata.php | 5 ++--- src/Symfony/Component/Validator/Mapping/ElementMetadata.php | 2 +- src/Symfony/Component/Validator/Mapping/MemberMetadata.php | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php index a7b836d3415ba..f7d7d563c3028 100644 --- a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php @@ -18,7 +18,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class AdHocMetadata extends ElementMetadata +class AdHocMetadata extends ElementMetadata implements MetadataInterface { public function __construct(array $constraints) { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 16c42ccd0826e..5712122445966 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -14,8 +14,7 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; -use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\GroupDefinitionException; @@ -26,7 +25,7 @@ * @author Bernhard Schussek * @author Fabien Potencier */ -class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface, ClassMetadataInterface +class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, PropertyMetadataContainerInterface, ClassMetadataInterface { /** * @var string diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index cfe34e3985f5b..9dedb79fd95e4 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraint; -abstract class ElementMetadata implements MetadataInterface +abstract class ElementMetadata { /** * @var Constraint[] diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index f3b3e3cf8fed2..876af1ae808a0 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -177,7 +177,7 @@ public function getTraversalStrategy() */ public function isCascaded() { - return $this->cascadingStrategy & CascadingStrategy::CASCADE; + return (boolean) ($this->cascadingStrategy & CascadingStrategy::CASCADE); } /** @@ -188,7 +188,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return $this->traversalStrategy & TraversalStrategy::TRAVERSE; + return (boolean) ($this->traversalStrategy & TraversalStrategy::TRAVERSE); } /** @@ -199,7 +199,7 @@ public function isCollectionCascaded() */ public function isCollectionCascadedDeeply() { - return $this->traversalStrategy & TraversalStrategy::RECURSIVE; + return (boolean) ($this->traversalStrategy & TraversalStrategy::RECURSIVE); } /** From 499b2bb601cc46d5fda7de625702aee276ff738c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:50:05 +0100 Subject: [PATCH 026/323] [Validator] Completed test coverage of ExecutionContext --- .../Tests/Context/ExecutionContextTest.php | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php new file mode 100644 index 0000000000000..413e3fbc7d81c --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Context; + +use Symfony\Component\Validator\Context\ExecutionContext; +use Symfony\Component\Validator\Mapping\AdHocMetadata; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\ValueNode; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class ExecutionContextTest extends \PHPUnit_Framework_TestCase +{ + const ROOT = '__ROOT__'; + + const TRANSLATION_DOMAIN = '__TRANSLATION_DOMAIN__'; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $validator; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $groupManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $translator; + + /** + * @var ExecutionContext + */ + private $context; + + protected function setUp() + { + $this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface'); + $this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface'); + $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); + + $this->context = new ExecutionContext( + self::ROOT, + $this->validator, + $this->groupManager, + $this->translator, + self::TRANSLATION_DOMAIN + ); + } + + public function testPushAndPop() + { + $metadata = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); + $node = new ValueNode('value', $metadata, '', array(), array()); + + $this->context->pushNode($node); + + $this->assertSame('value', $this->context->getValue()); + // the other methods are covered in AbstractValidatorTest + + $this->assertSame($node, $this->context->popNode()); + + $this->assertNull($this->context->getValue()); + } + + public function testPushTwiceAndPop() + { + $metadata1 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); + $node1 = new ValueNode('value', $metadata1, '', array(), array()); + $metadata2 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); + $node2 = new ValueNode('other value', $metadata2, '', array(), array()); + + $this->context->pushNode($node1); + $this->context->pushNode($node2); + + $this->assertSame($node2, $this->context->popNode()); + + $this->assertSame('value', $this->context->getValue()); + } + + public function testPopWithoutPush() + { + $this->assertNull($this->context->popNode()); + } + + public function testGetGroup() + { + $this->groupManager->expects($this->once()) + ->method('getCurrentGroup') + ->will($this->returnValue('Current Group')); + + $this->assertSame('Current Group', $this->context->getGroup()); + } +} From 405a03b365ed13acea72b23cbf9b1cbc54ff9592 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:58:10 +0100 Subject: [PATCH 027/323] [Validator] Updated deprecation notes in GroupSequence --- .../Validator/Constraints/GroupSequence.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index e2fe768c35648..10dec2ef83f7b 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -94,12 +94,12 @@ public function __construct(array $groups) /** * Returns an iterator for this group. * - * @return Traversable The iterator + * @return \Traversable The iterator * * @see \IteratorAggregate::getIterator() * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function getIterator() { @@ -113,8 +113,8 @@ public function getIterator() * * @return Boolean Whether the offset exists * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetExists($offset) { @@ -130,8 +130,8 @@ public function offsetExists($offset) * * @throws OutOfBoundsException If the object does not exist * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetGet($offset) { @@ -151,8 +151,8 @@ public function offsetGet($offset) * @param integer $offset The offset * @param string $value The group name * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetSet($offset, $value) { @@ -170,8 +170,8 @@ public function offsetSet($offset, $value) * * @param integer $offset The offset * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetUnset($offset) { @@ -183,8 +183,8 @@ public function offsetUnset($offset) * * @return integer The number of groups * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function count() { From 9b07b0c67271225d750746cace5687b7b654d251 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 18:03:59 +0100 Subject: [PATCH 028/323] [Validator] Implemented BC validation of arrays through validate() --- .../Validator/Constraints/Traverse.php | 56 ++++++ .../Component/Validator/Constraints/Valid.php | 13 +- .../Validator/Context/ExecutionContext.php | 17 ++ .../Context/ExecutionContextInterface.php | 2 +- .../Context/LegacyExecutionContext.php | 28 ++- .../Validator/Mapping/AdHocMetadata.php | 59 ------- .../Validator/Mapping/ClassMetadata.php | 24 ++- .../Validator/Mapping/ElementMetadata.php | 93 +--------- .../Validator/Mapping/GenericMetadata.php | 164 ++++++++++++++++++ .../Validator/Mapping/MemberMetadata.php | 29 +--- .../Node/{ValueNode.php => GenericNode.php} | 2 +- .../Tests/Context/ExecutionContextTest.php | 10 +- .../Tests/Validator/AbstractValidatorTest.php | 55 ++++-- .../Tests/Validator/LegacyValidatorTest.php | 10 ++ .../Tests/Validator/ValidatorTest.php | 44 +++++ .../Validator/Validator/AbstractValidator.php | 28 ++- .../Validator/Validator/LegacyValidator.php | 9 +- .../Validator/Validator/Validator.php | 2 +- 18 files changed, 426 insertions(+), 219 deletions(-) create mode 100644 src/Symfony/Component/Validator/Constraints/Traverse.php delete mode 100644 src/Symfony/Component/Validator/Mapping/AdHocMetadata.php create mode 100644 src/Symfony/Component/Validator/Mapping/GenericMetadata.php rename src/Symfony/Component/Validator/Node/{ValueNode.php => GenericNode.php} (92%) diff --git a/src/Symfony/Component/Validator/Constraints/Traverse.php b/src/Symfony/Component/Validator/Constraints/Traverse.php new file mode 100644 index 0000000000000..d9afe60c0745b --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Traverse.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\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @Annotation + * + * @since 2.5 + * @author Bernhard Schussek + */ +class Traverse extends Constraint +{ + public $traverse = true; + + public $deep = false; + + public function __construct($options = null) + { + if (is_array($options) && array_key_exists('groups', $options)) { + throw new ConstraintDefinitionException(sprintf( + 'The option "groups" is not supported by the constraint %s', + __CLASS__ + )); + } + + parent::__construct($options); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOption() + { + return 'traverse'; + } + + /** + * {@inheritdoc} + */ + public function getTargets() + { + return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); + } +} diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index ab4676d3dfea7..6e84e9a5f053f 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -21,12 +21,8 @@ * * @api */ -class Valid extends Constraint +class Valid extends Traverse { - public $traverse = true; - - public $deep = false; - public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { @@ -35,4 +31,11 @@ public function __construct($options = null) parent::__construct($options); } + + public function getDefaultOption() + { + // Traverse is extended for backwards compatibility reasons + // The parent class should be removed in 3.0 + return null; + } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index bc6abc39b1d53..5b031aabbd0dc 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -80,6 +80,23 @@ class ExecutionContext implements ExecutionContextInterface */ private $translationDomain; + /** + * Creates a new execution context. + * + * @param mixed $root The root value of the + * validated object graph + * @param ValidatorInterface $validator The validator + * @param GroupManagerInterface $groupManager The manager for accessing + * the currently validated + * group + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain to + * use for translating + * violation messages + * + * @internal Called by {@link ExecutionContextManager}. Should not be used + * in user code. + */ public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { $this->root = $root; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 5ff0a817243f7..cbbc3c7447112 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -148,7 +148,7 @@ public function getValue(); * 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. The metadata can - * also be an {@link Mapping\AdHocMetadata} if the current value does not + * also be an {@link Mapping\GenericMetadata} if the current value does not * belong to any structural element. * * @return MetadataInterface|null The metadata of the currently validated diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 8cf17f5ee3ec5..ad38accc6265d 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -19,11 +19,25 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** - * @since %%NextVersion%% + * A backwards compatible execution context. + * + * @since 2.5 * @author Bernhard Schussek + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be + * removed in 3.0. */ class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface { + /** + * Creates a new context. + * + * This constructor ensures that the given validator implements the + * deprecated {@link \Symfony\Component\Validator\ValidatorInterface}. If + * it does not, an {@link InvalidArgumentException} is thrown. + * + * @see ExecutionContext::__construct() + */ public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { if (!$validator instanceof LegacyValidatorInterface) { @@ -56,6 +70,9 @@ public function addViolation($message, array $parameters = array(), $invalidValu parent::addViolation($message, $parameters); } + /** + * {@inheritdoc} + */ public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { if (func_num_args() >= 3) { @@ -78,6 +95,9 @@ public function addViolationAt($subPath, $message, array $parameters = array(), ; } + /** + * {@inheritdoc} + */ public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { // TODO handle $traverse and $deep @@ -90,6 +110,9 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ; } + /** + * {@inheritdoc} + */ public function validateValue($value, $constraints, $subPath = '', $groups = null) { return $this @@ -100,6 +123,9 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul ; } + /** + * {@inheritdoc} + */ public function getMetadataFactory() { return $this->getValidator()->getMetadataFactory(); diff --git a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php deleted file mode 100644 index f7d7d563c3028..0000000000000 --- a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php +++ /dev/null @@ -1,59 +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\Mapping; - -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Exception\ValidatorException; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -class AdHocMetadata extends ElementMetadata implements MetadataInterface -{ - public function __construct(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) - )); - } - - $this->addConstraint($constraint); - } - } - - public function getCascadingStrategy() - { - - } - - public function getTraversalStrategy() - { - - } -} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 5712122445966..d3c775b579cf1 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; @@ -62,8 +63,6 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, */ public $groupSequenceProvider = false; - public $traversalStrategy = TraversalStrategy::IMPLICIT; - /** * @var \ReflectionClass */ @@ -126,7 +125,12 @@ public function accept(ValidationVisitorInterface $visitor, $value, $group, $pro */ public function __sleep() { - return array_merge(parent::__sleep(), array( + $parentProperties = parent::__sleep(); + + // Don't store the cascading strategy. Classes never cascade. + unset($parentProperties[array_search('cascadingStrategy', $parentProperties)]); + + return array_merge($parentProperties, array( 'getters', 'groupSequence', 'groupSequenceProvider', @@ -174,7 +178,14 @@ public function addConstraint(Constraint $constraint) { if (!in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets())) { throw new ConstraintDefinitionException(sprintf( - 'The constraint %s cannot be put on classes', + 'The constraint "%s" cannot be put on classes.', + get_class($constraint) + )); + } + + if ($constraint instanceof Valid) { + throw new ConstraintDefinitionException(sprintf( + 'The constraint "%s" cannot be put on classes.', get_class($constraint) )); } @@ -434,9 +445,4 @@ public function getCascadingStrategy() { return CascadingStrategy::NONE; } - - public function getTraversalStrategy() - { - return $this->traversalStrategy; - } } diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index 9dedb79fd95e4..84a826aa10f7d 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -11,97 +11,6 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Constraint; - -abstract class ElementMetadata +abstract class ElementMetadata extends GenericMetadata { - /** - * @var Constraint[] - */ - public $constraints = array(); - - /** - * @var array - */ - public $constraintsByGroup = array(); - - /** - * Returns the names of the properties that should be serialized. - * - * @return array - */ - public function __sleep() - { - return array( - 'constraints', - 'constraintsByGroup', - ); - } - - /** - * Clones this object. - */ - public function __clone() - { - $constraints = $this->constraints; - - $this->constraints = array(); - $this->constraintsByGroup = array(); - - foreach ($constraints as $constraint) { - $this->addConstraint(clone $constraint); - } - } - - /** - * Adds a constraint to this element. - * - * @param Constraint $constraint - * - * @return ElementMetadata - */ - public function addConstraint(Constraint $constraint) - { - $this->constraints[] = $constraint; - - foreach ($constraint->groups as $group) { - $this->constraintsByGroup[$group][] = $constraint; - } - - return $this; - } - - /** - * Returns all constraints of this element. - * - * @return Constraint[] An array of Constraint instances - */ - public function getConstraints() - { - return $this->constraints; - } - - /** - * Returns whether this element has any constraints. - * - * @return Boolean - */ - public function hasConstraints() - { - return count($this->constraints) > 0; - } - - /** - * Returns the constraints of the given group and global ones (* group). - * - * @param string $group The group name - * - * @return array An array with all Constraint instances belonging to the group - */ - public function findConstraints($group) - { - return isset($this->constraintsByGroup[$group]) - ? $this->constraintsByGroup[$group] - : array(); - } } diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php new file mode 100644 index 0000000000000..75b07c8825ccf --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -0,0 +1,164 @@ + + * + * 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\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class GenericMetadata implements MetadataInterface +{ + /** + * @var Constraint[] + */ + public $constraints = array(); + + /** + * @var array + */ + public $constraintsByGroup = array(); + + public $cascadingStrategy = CascadingStrategy::NONE; + + public $traversalStrategy = TraversalStrategy::IMPLICIT; + + /** + * Returns the names of the properties that should be serialized. + * + * @return array + */ + public function __sleep() + { + return array( + 'constraints', + 'constraintsByGroup', + 'cascadingStrategy', + 'traversalStrategy', + ); + } + + /** + * Clones this object. + */ + public function __clone() + { + $constraints = $this->constraints; + + $this->constraints = array(); + $this->constraintsByGroup = array(); + + foreach ($constraints as $constraint) { + $this->addConstraint(clone $constraint); + } + } + + /** + * Adds a constraint to this element. + * + * @param Constraint $constraint + * + * @return ElementMetadata + */ + public function addConstraint(Constraint $constraint) + { + if ($constraint instanceof Valid) { + $this->cascadingStrategy = CascadingStrategy::CASCADE; + + // Continue. Valid extends Traverse, so the return statement in the + // next block is going be executed. + } + + if ($constraint instanceof Traverse) { + if (true === $constraint->traverse) { + // If traverse is true, traversal should be explicitly enabled + $this->traversalStrategy = TraversalStrategy::TRAVERSE; + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } + } elseif (false === $constraint->traverse) { + // If traverse is false, traversal should be explicitly disabled + $this->traversalStrategy = TraversalStrategy::NONE; + } else { + // Else, traverse depending on the contextual information that + // is available during validation + $this->traversalStrategy = TraversalStrategy::IMPLICIT; + } + + // The constraint is not added + return $this; + } + + $this->constraints[] = $constraint; + + foreach ($constraint->groups as $group) { + $this->constraintsByGroup[$group][] = $constraint; + } + + return $this; + } + + public function addConstraints(array $constraints) + { + foreach ($constraints as $constraint) { + $this->addConstraint($constraint); + } + } + + /** + * Returns all constraints of this element. + * + * @return Constraint[] An array of Constraint instances + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * Returns whether this element has any constraints. + * + * @return Boolean + */ + public function hasConstraints() + { + return count($this->constraints) > 0; + } + + /** + * Returns the constraints of the given group and global ones (* group). + * + * @param string $group The group name + * + * @return array An array with all Constraint instances belonging to the group + */ + public function findConstraints($group) + { + return isset($this->constraintsByGroup[$group]) + ? $this->constraintsByGroup[$group] + : array(); + } + + public function getCascadingStrategy() + { + return $this->cascadingStrategy; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 876af1ae808a0..80a2687458ddb 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -14,7 +14,6 @@ use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, LegacyPropertyMetadataInterface @@ -22,8 +21,6 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat public $class; public $name; public $property; - public $cascadingStrategy = CascadingStrategy::NONE; - public $traversalStrategy = TraversalStrategy::IMPLICIT; private $reflMember = array(); /** @@ -61,19 +58,7 @@ public function addConstraint(Constraint $constraint) )); } - if ($constraint instanceof Valid) { - $this->cascadingStrategy = CascadingStrategy::CASCADE; - - if ($constraint->traverse) { - $this->traversalStrategy = TraversalStrategy::TRAVERSE; - } - - if ($constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::RECURSIVE; - } - } else { - parent::addConstraint($constraint); - } + parent::addConstraint($constraint); return $this; } @@ -89,8 +74,6 @@ public function __sleep() 'class', 'name', 'property', - 'cascadingStrategy', - 'traversalStrategy', )); } @@ -160,16 +143,6 @@ public function isPrivate($objectOrClassName) return $this->getReflectionMember($objectOrClassName)->isPrivate(); } - public function getCascadingStrategy() - { - return $this->cascadingStrategy; - } - - public function getTraversalStrategy() - { - return $this->traversalStrategy; - } - /** * Returns whether objects stored in this member should be validated * diff --git a/src/Symfony/Component/Validator/Node/ValueNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php similarity index 92% rename from src/Symfony/Component/Validator/Node/ValueNode.php rename to src/Symfony/Component/Validator/Node/GenericNode.php index e0f77e41f19c0..aab73db64e235 100644 --- a/src/Symfony/Component/Validator/Node/ValueNode.php +++ b/src/Symfony/Component/Validator/Node/GenericNode.php @@ -15,6 +15,6 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ValueNode extends Node +class GenericNode extends Node { } diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 413e3fbc7d81c..54e3374f66bf7 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -12,10 +12,10 @@ namespace Symfony\Component\Validator\Tests\Context; use Symfony\Component\Validator\Context\ExecutionContext; -use Symfony\Component\Validator\Mapping\AdHocMetadata; +use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\ValueNode; +use Symfony\Component\Validator\Node\GenericNode; /** * @since 2.5 @@ -65,7 +65,7 @@ protected function setUp() public function testPushAndPop() { $metadata = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node = new ValueNode('value', $metadata, '', array(), array()); + $node = new GenericNode('value', $metadata, '', array(), array()); $this->context->pushNode($node); @@ -80,9 +80,9 @@ public function testPushAndPop() public function testPushTwiceAndPop() { $metadata1 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node1 = new ValueNode('value', $metadata1, '', array(), array()); + $node1 = new GenericNode('value', $metadata1, '', array(), array()); $metadata2 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node2 = new ValueNode('other value', $metadata2, '', array(), array()); + $node2 = new GenericNode('other value', $metadata2, '', array(), array()); $this->context->pushNode($node1); $this->context->pushNode($node2); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index c3148813202d8..d8a1c43500561 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -42,22 +42,22 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase /** * @var ValidatorInterface */ - private $validator; + protected $validator; /** * @var FakeMetadataFactory */ - public $metadataFactory; + protected $metadataFactory; /** * @var ClassMetadata */ - public $metadata; + protected $metadata; /** * @var ClassMetadata */ - public $referenceMetadata; + protected $referenceMetadata; protected function setUp() { @@ -199,6 +199,45 @@ public function testGetterConstraint() $this->assertNull($violations[0]->getCode()); } + public function testArray() + { + $test = $this; + $entity = new Entity(); + $array = array('key' => $entity); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + public function testReferenceClassConstraint() { $test = $this; @@ -815,14 +854,6 @@ public function testValidateValue() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidateValueRejectsValid() - { - $this->validator->validateValue(new Entity(), new Valid()); - } - public function testAddCustomizedViolation() { $entity = new Entity(); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index 327194d751392..eafbc4d1282f7 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; @@ -37,4 +39,12 @@ public function testGroupSequenceIncludesReferences() { $this->markTestSkipped('Currently not supported'); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidateValueRejectsValid() + { + $this->validator->validateValue(new Entity(), new Valid()); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php index f990f10e04c3c..57a08fbc94960 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextManager; @@ -18,6 +22,7 @@ use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validator\LegacyValidator; class ValidatorTest extends AbstractValidatorTest @@ -44,4 +49,43 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) return $validator; } + + public function testValidateValueAcceptsValid() + { + $test = $this; + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + // This is the same as when calling validateObject() + $violations = $this->validator->validateValue($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } } diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index c57473e751d89..959e98779f81c 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -12,15 +12,16 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\AdHocMetadata; +use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\Node\ValueNode; +use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\Util\PropertyPath; @@ -97,6 +98,24 @@ protected function traverseObject($object, $groups = null) ))); } + protected function traverseCollection($collection, $groups = null, $deep = false) + { + $metadata = new GenericMetadata(); + $metadata->addConstraint(new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + ))); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->nodeTraverser->traverse(array(new GenericNode( + $collection, + $metadata, + $this->defaultPropertyPath, + $groups, + $groups + ))); + } + protected function traverseProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -165,10 +184,11 @@ protected function traverseValue($value, $constraints, $groups = null) $constraints = array($constraints); } - $metadata = new AdHocMetadata($constraints); + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $this->nodeTraverser->traverse(array(new ValueNode( + $this->nodeTraverser->traverse(array(new GenericNode( $value, $metadata, $this->defaultPropertyPath, diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index f8a2cc74c1bc8..b2fb4e5d35daf 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -21,7 +21,14 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { - // TODO what about $traverse and $deep? + if (is_array($value)) { + $this->contextManager->startContext($value); + + $this->traverseCollection($value, $groups, $deep); + + return $this->contextManager->stopContext()->getViolations(); + } + return $this->validateObject($value, $groups); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 0c0c38880b33d..fe55818e7069c 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -24,7 +24,7 @@ class Validator extends AbstractValidator /** * @var ExecutionContextManagerInterface */ - private $contextManager; + protected $contextManager; public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextManagerInterface $contextManager) { From 297ba4f585b7c08a4520f62e8554bf2824e52831 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 18:07:44 +0100 Subject: [PATCH 029/323] [Validator] Added a note why scalars are passed to cascadeObject() in NodeTraverser --- .../Component/Validator/NodeTraverser/NodeTraverser.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index ce1c6029b5aee..d0529af0050e9 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -113,6 +113,9 @@ private function traverseNode(Node $node) $traversalStrategy ); } elseif ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) $this->cascadeObject( $node->value, $node->propertyPath, From 09f744b89cb064b2cc137d2aa8ffcfe13e4c2bdc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 18:21:00 +0100 Subject: [PATCH 030/323] [Validator] Implemented BC traversal of traversables through validate() --- .../Tests/Validator/AbstractValidatorTest.php | 163 ++++++++++++++++++ .../Validator/Validator/AbstractValidator.php | 18 -- .../Validator/Validator/LegacyValidator.php | 16 +- 3 files changed, 175 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index d8a1c43500561..2b55290a1d999 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -238,6 +238,169 @@ public function testArray() $this->assertNull($violations[0]->getCode()); } + public function testRecursiveArray() + { + $test = $this; + $entity = new Entity(); + $array = array(2 => array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + + public function testTraversableTraverseEnabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group', true); + } + + public function testRecursiveTraversableRecursiveTraversalEnabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($traversable, 'Group', true, true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + public function testReferenceClassConstraint() { $test = $this; diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index 959e98779f81c..f0de724ffe162 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -98,24 +98,6 @@ protected function traverseObject($object, $groups = null) ))); } - protected function traverseCollection($collection, $groups = null, $deep = false) - { - $metadata = new GenericMetadata(); - $metadata->addConstraint(new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - ))); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $collection, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - protected function traverseProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index b2fb4e5d35daf..9586f200dd7cb 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** @@ -22,11 +24,17 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface public function validate($value, $groups = null, $traverse = false, $deep = false) { if (is_array($value)) { - $this->contextManager->startContext($value); - - $this->traverseCollection($value, $groups, $deep); + return $this->validateValue($value, new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )), $groups); + } - return $this->contextManager->stopContext()->getViolations(); + if ($traverse && $value instanceof \Traversable) { + return $this->validateValue($value, array( + new Valid(), + new Traverse(array('traverse' => true, 'deep' => $deep)), + ), $groups); } return $this->validateObject($value, $groups); From ee1adadbfb8b620900028089135c575f20733dc7 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 11:56:27 +0100 Subject: [PATCH 031/323] [Validator] Implemented handling of arrays and Traversables in LegacyExecutionContext::validate() --- .../Context/LegacyExecutionContext.php | 30 +++++- .../Tests/Validator/AbstractValidatorTest.php | 95 +++++++++++++++++++ .../Validator/Validator/AbstractValidator.php | 2 - .../Validator/ContextualValidator.php | 15 +++ .../Validator/Validator/LegacyValidator.php | 12 ++- .../Validator/Validator/Validator.php | 15 +++ .../Validator/ValidatorInterface.php | 2 +- 7 files changed, 163 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index ad38accc6265d..db30eb92b5b6c 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; @@ -100,7 +102,33 @@ public function addViolationAt($subPath, $message, array $parameters = array(), */ public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { - // TODO handle $traverse and $deep + if (is_array($value)) { + $constraint = new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )); + + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateValue($value, $constraint, $groups) + ; + } + + if ($traverse && $value instanceof \Traversable) { + $constraints = array( + new Valid(), + new Traverse(array('traverse' => true, 'deep' => $deep)), + ); + + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateValue($value, $constraints, $groups) + ; + } return $this ->getValidator() diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 2b55290a1d999..b7a9ee2e19aee 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; @@ -1416,6 +1417,100 @@ public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); } + public function testValidateInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate($value->reference, 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate(array('key' => $value->reference), 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index f0de724ffe162..d4d4e62e9c976 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -12,9 +12,7 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 560f088522992..8a58f48d9fc04 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; @@ -41,6 +42,8 @@ public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFacto public function atPath($subPath) { $this->defaultPropertyPath = $this->context->getPropertyPath($subPath); + + return $this; } /** @@ -62,6 +65,18 @@ public function validateObject($object, $groups = null) return $this->context->getViolations(); } + public function validateCollection($collection, $groups = null, $deep = false) + { + $constraint = new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )); + + $this->traverseValue($collection, $constraint, $groups); + + return $this->context->getViolations(); + } + /** * Validates a property of a value against its current value. * diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 9586f200dd7cb..059d84c1daf9e 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -24,17 +24,21 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface public function validate($value, $groups = null, $traverse = false, $deep = false) { if (is_array($value)) { - return $this->validateValue($value, new Traverse(array( + $constraint = new Traverse(array( 'traverse' => true, 'deep' => $deep, - )), $groups); + )); + + return $this->validateValue($value, $constraint, $groups); } if ($traverse && $value instanceof \Traversable) { - return $this->validateValue($value, array( + $constraints = array( new Valid(), new Traverse(array('traverse' => true, 'deep' => $deep)), - ), $groups); + ); + + return $this->validateValue($value, $constraints, $groups); } return $this->validateObject($value, $groups); diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index fe55818e7069c..18022ba19f5c1 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; @@ -42,6 +43,20 @@ public function validateObject($object, $groups = null) return $this->contextManager->stopContext()->getViolations(); } + public function validateCollection($collection, $groups = null, $deep = false) + { + $this->contextManager->startContext($collection); + + $constraint = new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )); + + $this->traverseValue($collection, $constraint, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + public function validateProperty($object, $propertyName, $groups = null) { $this->contextManager->startContext($object); diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 7985a1f9ef4e8..6d1833d162129 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -35,7 +35,7 @@ interface ValidatorInterface */ public function validateObject($object, $groups = null); -// public function validateCollection($collection, $groups = null); + public function validateCollection($collection, $groups = null, $deep = false); /** * Validates a property of a value against its current value. From 718601c6c3642fa7a65a408016a34feef00fe44d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 12:02:54 +0100 Subject: [PATCH 032/323] [Validator] Changed validateValue() to validate() in the new API The validation of values against constraints should be a first-class citizen in the API. For this reason, validate() now takes a value and a constraint or a list of constraints. This method should be used for the most basic use cases. If users want to annotate objects with constraints (this is optional, advanced functionality), they can use the more expressive validateObject() method now. For traversing arrays or Traversables, a new method validateCollection() is now available in the API. --- .../Context/LegacyExecutionContext.php | 6 ++-- .../Validator/ContextualValidator.php | 34 +++++++++---------- .../Validator/Validator/LegacyValidator.php | 11 ++++++ .../Validator/Validator/Validator.php | 19 +++++------ .../Validator/ValidatorInterface.php | 24 ++++++------- 5 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index db30eb92b5b6c..0563bdab3d87b 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -112,7 +112,7 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateValue($value, $constraint, $groups) + ->validate($value, $constraint, $groups) ; } @@ -126,7 +126,7 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateValue($value, $constraints, $groups) + ->validate($value, $constraints, $groups) ; } @@ -147,7 +147,7 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateValue($value, $constraints, $groups) + ->validate($value, $constraints, $groups) ; } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 8a58f48d9fc04..a3e21863df7b4 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -46,6 +46,23 @@ public function atPath($subPath) return $this; } + /** + * Validates a value against a constraint or a list of constraints. + * + * @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 ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validate($value, $constraints, $groups = null) + { + $this->traverseValue($value, $constraints, $groups); + + return $this->context->getViolations(); + } + /** * Validates a value. * @@ -118,21 +135,4 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = return $this->context->getViolations(); } - - /** - * Validates a value against a constraint or a list of constraints. - * - * @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 ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ - public function validateValue($value, $constraints, $groups = null) - { - $this->traverseValue($value, $constraints, $groups); - - return $this->context->getViolations(); - } } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 059d84c1daf9e..64df69e1e26da 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -23,6 +24,11 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { + // Use new signature if constraints are given in the second argument + if (func_num_args() <= 3 && ($groups instanceof Constraint || (is_array($groups) && current($groups) instanceof Constraint))) { + return parent::validate($value, $groups, $traverse); + } + if (is_array($value)) { $constraint = new Traverse(array( 'traverse' => true, @@ -44,6 +50,11 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals return $this->validateObject($value, $groups); } + public function validateValue($value, $constraints, $groups = null) + { + return parent::validate($value, $constraints, $groups); + } + public function getMetadataFactory() { return $this->metadataFactory; diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 18022ba19f5c1..5d8b509be3eb6 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -34,6 +34,15 @@ public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFacto $this->contextManager = $contextManager; } + public function validate($value, $constraints, $groups = null) + { + $this->contextManager->startContext($value); + + $this->traverseValue($value, $constraints, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + public function validateObject($object, $groups = null) { $this->contextManager->startContext($object); @@ -74,14 +83,4 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = return $this->contextManager->stopContext()->getViolations(); } - - public function validateValue($value, $constraints, $groups = null) - { - $this->contextManager->startContext($value); - - $this->traverseValue($value, $constraints, $groups); - - return $this->contextManager->stopContext()->getViolations(); - } - } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 6d1833d162129..8cecaf3dc4b49 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -21,6 +21,18 @@ */ interface ValidatorInterface { + /** + * Validates a value against a constraint or a list of constraints. + * + * @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 ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validate($value, $constraints, $groups = null); + /** * Validates a value. * @@ -69,18 +81,6 @@ public function validateProperty($object, $propertyName, $groups = null); */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); - /** - * Validates a value against a constraint or a list of constraints. - * - * @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 ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ - public function validateValue($value, $constraints, $groups = null); - /** * @param ExecutionContextInterface $context * From feb3d6f20233001c4d04e4c7ded948e554a58432 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:04:59 +0100 Subject: [PATCH 033/323] [Validator] Tested the validation in a separate context --- .../Validator/Context/ExecutionContext.php | 18 +- .../Context/ExecutionContextManager.php | 30 +--- .../Validator/NodeVisitor/NodeValidator.php | 2 - .../Tests/Validator/AbstractValidatorTest.php | 164 ++++++++++++++++++ .../Tests/Validator/LegacyValidatorTest.php | 25 ++- .../Tests/Validator/ValidatorTest.php | 10 +- 6 files changed, 206 insertions(+), 43 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 5b031aabbd0dc..e2b582843f28e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -137,19 +137,13 @@ public function popNode() return null; } - $poppedNode = $this->node; + // Remove the current node from the stack + $poppedNode = $this->nodeStack->pop(); - // After removing the last node, the stack is empty and the node - // is null - if (1 === count($this->nodeStack)) { - $this->nodeStack->pop(); - $this->node = null; - - return $poppedNode; - } - - $this->nodeStack->pop(); - $this->node = $this->nodeStack->top(); + // Adjust the current node to the previous node + $this->node = count($this->nodeStack) > 0 + ? $this->nodeStack->top() + : null; return $poppedNode; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 369b6a062b265..5625136b36f26 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -72,10 +72,6 @@ public function startContext($root) // TODO error, call initialize() first } - if (null !== $this->currentContext) { - $this->contextStack->push($this->currentContext); - } - $this->currentContext = new LegacyExecutionContext( $root, $this->validator, @@ -83,29 +79,24 @@ public function startContext($root) $this->translator, $this->translationDomain ); + $this->contextStack->push($this->currentContext); return $this->currentContext; } public function stopContext() { - $stoppedContext = $this->currentContext; - if (0 === count($this->contextStack)) { - $this->currentContext = null; - - return $stoppedContext; + return null; } - if (1 === count($this->contextStack)) { - $this->contextStack->pop(); - $this->currentContext = null; - - return $stoppedContext; - } + // Remove the current context from the stack + $stoppedContext = $this->contextStack->pop(); - $this->contextStack->pop(); - $this->currentContext = $this->contextStack->top(); + // Adjust the current context to the previous context + $this->currentContext = count($this->contextStack) > 0 + ? $this->contextStack->top() + : null; return $stoppedContext; } @@ -115,11 +106,6 @@ public function getCurrentContext() return $this->currentContext; } - public function afterTraversal(array $nodes) - { - $this->contextStack = new \SplStack(); - } - public function enterNode(Node $node) { if (null === $this->currentContext) { diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 662262a4a5bf1..100c9b7d3b538 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -47,8 +47,6 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface private $currentGroup; - private $currentObjectHash; - private $objectHashStack; public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index b7a9ee2e19aee..60e5ebf76e8c2 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -1423,6 +1423,58 @@ public function testValidateInContext() $entity = new Entity(); $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateObject($value->reference) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateInContextLegacyApi() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { $context->validate($value->reference, 'subpath'); }; @@ -1470,6 +1522,58 @@ public function testValidateArrayInContext() $entity = new Entity(); $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateCollection(array('key' => $value->reference)) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContextLegacyApi() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { $context->validate(array('key' => $value->reference), 'subpath'); }; @@ -1511,6 +1615,66 @@ public function testValidateArrayInContext() $this->assertNull($violations[0]->getCode()); } + public function testValidateInSeparateContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $violations = $context + ->getValidator() + // Since the validator is not context aware, the group must + // be passed explicitly + ->validateObject($value->reference, 'Group') + ; + + /** @var ConstraintViolationInterface[] $violations */ + $test->assertCount(1, $violations); + $test->assertSame('Message value', $violations[0]->getMessage()); + $test->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $test->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $test->assertSame('', $violations[0]->getPropertyPath()); + // The root is different as we're in a new context + $test->assertSame($entity->reference, $violations[0]->getRoot()); + $test->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $test->assertNull($violations[0]->getMessagePluralization()); + $test->assertNull($violations[0]->getCode()); + + // Verify that this method is called + $context->addViolation('Separate violation'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity->reference, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $test->assertSame('Separate violation', $violations[0]->getMessage()); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index eafbc4d1282f7..c66cde30ea886 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -14,7 +14,7 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Validator as LegacyValidator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; @@ -22,22 +22,37 @@ class LegacyValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { - return new Validator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); + return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); } public function testNoDuplicateValidationIfConstraintInMultipleGroups() { - $this->markTestSkipped('Currently not supported'); + $this->markTestSkipped('Not supported in the legacy API'); } public function testGroupSequenceAbortsAfterFailedGroup() { - $this->markTestSkipped('Currently not supported'); + $this->markTestSkipped('Not supported in the legacy API'); } public function testGroupSequenceIncludesReferences() { - $this->markTestSkipped('Currently not supported'); + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testValidateInContext() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testValidateArrayInContext() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testValidateInSeparateContext() + { + $this->markTestSkipped('Not supported in the legacy API'); } /** diff --git a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php index 57a08fbc94960..2b0d00205e887 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php @@ -24,9 +24,15 @@ use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validator\LegacyValidator; +use Symfony\Component\Validator\Validator\ValidatorInterface; class ValidatorTest extends AbstractValidatorTest { + /** + * @var ValidatorInterface + */ + protected $validator; + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); @@ -50,7 +56,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) return $validator; } - public function testValidateValueAcceptsValid() + public function testValidateAcceptsValid() { $test = $this; $entity = new Entity(); @@ -75,7 +81,7 @@ public function testValidateValueAcceptsValid() ))); // This is the same as when calling validateObject() - $violations = $this->validator->validateValue($entity, new Valid(), 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); From 1e81f3bdc8dad2ba63fc8a7d88caca7f0a42f420 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:28:37 +0100 Subject: [PATCH 034/323] [Validator] Finished test coverage and documentation of ExecutionContextManager --- .../Context/ExecutionContextManager.php | 75 ++++++++++++++- .../ExecutionContextManagerInterface.php | 54 ++++++++++- .../Context/LegacyExecutionContext.php | 2 +- .../Context/ExecutionContextManagerTest.php | 95 +++++++++++++++++++ .../Tests/Context/ExecutionContextTest.php | 3 - 5 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 5625136b36f26..decefdc8a881d 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -12,14 +12,29 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; use Symfony\Component\Validator\Validator\ValidatorInterface; /** - * @since %%NextVersion%% + * The default implementation of {@link ExecutionContextManagerInterface}. + * + * This class implements {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface} + * and updates the current context with the current node of the validation + * traversal. + * + * After creating a new instance, the method {@link initialize()} must be + * called with a {@link ValidatorInterface} instance. Calling methods such as + * {@link startContext()} or {@link enterNode()} without initializing the + * manager first will lead to errors. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see ExecutionContextManagerInterface + * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface */ class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface { @@ -53,6 +68,17 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex */ private $translationDomain; + /** + * Creates a new context manager. + * + * @param GroupManagerInterface $groupManager The manager for accessing + * the currently validated + * group + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain to + * use for translating + * violation messages + */ public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { $this->groupManager = $groupManager; @@ -61,15 +87,27 @@ public function __construct(GroupManagerInterface $groupManager, TranslatorInter $this->contextStack = new \SplStack(); } + /** + * Initializes the manager with a validator. + * + * @param ValidatorInterface $validator The validator + */ public function initialize(ValidatorInterface $validator) { $this->validator = $validator; } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ public function startContext($root) { if (null === $this->validator) { - // TODO error, call initialize() first + throw new RuntimeException( + 'initialize() must be called before startContext().' + ); } $this->currentContext = new LegacyExecutionContext( @@ -84,10 +122,18 @@ public function startContext($root) return $this->currentContext; } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link startContext()} wasn't called + */ public function stopContext() { if (0 === count($this->contextStack)) { - return null; + throw new RuntimeException( + 'No context was started yet. Call startContext() before '. + 'stopContext().' + ); } // Remove the current context from the stack @@ -101,24 +147,43 @@ public function stopContext() return $stoppedContext; } + /** + * {@inheritdoc} + */ public function getCurrentContext() { return $this->currentContext; } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ public function enterNode(Node $node) { if (null === $this->currentContext) { - // TODO error call startContext() first + throw new RuntimeException( + 'No context was started yet. Call startContext() before '. + 'enterNode().' + ); } $this->currentContext->pushNode($node); } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ public function leaveNode(Node $node) { if (null === $this->currentContext) { - // error no context started + throw new RuntimeException( + 'No context was started yet. Call startContext() before '. + 'leaveNode().' + ); } $this->currentContext->popNode(); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php index b805c12e43364..a2dd04d7dccd5 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -12,24 +12,74 @@ namespace Symfony\Component\Validator\Context; /** - * @since %%NextVersion%% + * Manages the creation and deletion of {@link ExecutionContextInterface} + * instances. + * + * Start a new context with {@link startContext()}. You can retrieve the context + * with {@link getCurrentContext()} and stop it again with {@link stopContext()}. + * + * $contextManager->startContext(); + * $context = $contextManager->getCurrentContext(); + * $contextManager->stopContext(); + * + * You can also start several nested contexts. The {@link getCurrentContext()} + * method will always return the most recently started context. + * + * // Start context 1 + * $contextManager->startContext(); + * + * // Start context 2 + * $contextManager->startContext(); + * + * // Returns context 2 + * $context = $contextManager->getCurrentContext(); + * + * // Stop context 2 + * $contextManager->stopContext(); + * + * // Returns context 1 + * $context = $contextManager->getCurrentContext(); + * + * See also {@link ExecutionContextInterface} for more information. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see ExecutionContextInterface */ interface ExecutionContextManagerInterface { /** - * @param mixed $root + * Starts a new context. + * + * The newly started context is returned. You can subsequently access the + * context with {@link getCurrentContext()}. + * + * @param mixed $root The root value of the object graph in the new context * * @return ExecutionContextInterface The started context */ public function startContext($root); /** + * Stops the current context. + * + * If multiple contexts have been started, the most recently started context + * is stopped. The stopped context is returned from this method. + * + * After calling this method, {@link getCurrentContext()} will return the + * context that was started before the stopped context. + * * @return ExecutionContextInterface The stopped context */ public function stopContext(); /** + * Returns the current context. + * + * If multiple contexts have been started, the current context refers to the + * most recently started context. + * * @return ExecutionContextInterface The current context */ public function getCurrentContext(); diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 0563bdab3d87b..685f06931bb61 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -21,7 +21,7 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** - * A backwards compatible execution context. + * An execution context that is compatible with the legacy API (< 2.5). * * @since 2.5 * @author Bernhard Schussek diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php new file mode 100644 index 0000000000000..548a11170d57d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Context; + +use Symfony\Component\Validator\Context\ExecutionContextManager; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class ExecutionContextManagerTest extends \PHPUnit_Framework_TestCase +{ + const TRANSLATION_DOMAIN = '__TRANSLATION_DOMAIN__'; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $validator; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $groupManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $translator; + + /** + * @var ExecutionContextManager + */ + private $contextManager; + + protected function setUp() + { + $this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface'); + $this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface'); + $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); + $this->contextManager = new ExecutionContextManager( + $this->groupManager, + $this->translator, + self::TRANSLATION_DOMAIN + ); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testInitializeMustBeCalledBeforeStartContext() + { + $this->contextManager->startContext('root'); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testCannotStopContextIfNoneWasStarted() + { + $this->contextManager->stopContext(); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testCannotEnterNodeWithoutActiveContext() + { + $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') + ->disableOriginalConstructor() + ->getMock(); + + $this->contextManager->enterNode($node); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testCannotLeaveNodeWithoutActiveContext() + { + $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') + ->disableOriginalConstructor() + ->getMock(); + + $this->contextManager->leaveNode($node); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 54e3374f66bf7..1133f413a7cd6 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -12,9 +12,6 @@ namespace Symfony\Component\Validator\Tests\Context; use Symfony\Component\Validator\Context\ExecutionContext; -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\GenericNode; /** From 9c9e715ca82c74fc9b657e3dd702d9134935d13b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:31:44 +0100 Subject: [PATCH 035/323] [Validator] Completed documentation of GroupManagerInterface --- .../Validator/Group/GroupManagerInterface.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php index 94a0ab9544e10..a231b907d55f3 100644 --- a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php +++ b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php @@ -12,10 +12,18 @@ namespace Symfony\Component\Validator\Group; /** - * @since %%NextVersion%% + * Returns the group that is currently being validated. + * + * @since 2.5 * @author Bernhard Schussek */ interface GroupManagerInterface { + /** + * Returns the group that is currently being validated. + * + * @return string|null The current group or null, if no validation is + * active. + */ public function getCurrentGroup(); } From 2c65a28608b54acca7a1d311c804aa8ceb8efc29 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:50:19 +0100 Subject: [PATCH 036/323] [Validator] Completed test coverage and documentation of the Node classes --- .../Component/Validator/Node/ClassNode.php | 30 +++++++++++--- .../Component/Validator/Node/GenericNode.php | 9 +++- src/Symfony/Component/Validator/Node/Node.php | 41 ++++++++++++++++++- .../Component/Validator/Node/PropertyNode.php | 28 ++++++++++++- .../Validator/Tests/Node/ClassNodeTest.php | 31 ++++++++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index d49bf81c77b80..07554963d7feb 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -11,10 +11,13 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; /** - * @since %%NextVersion%% + * Represents an object and its class metadata in the validation graph. + * + * @since 2.5 * @author Bernhard Schussek */ class ClassNode extends Node @@ -24,19 +27,34 @@ class ClassNode extends Node */ public $metadata; - public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + /** + * Creates a new class node. + * + * @param object $object The validated object + * @param ClassMetadataInterface $metadata The class metadata of that + * object + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[] $cascadedGroups The groups in which + * cascaded objects should be + * validated + * + * @throws UnexpectedTypeException If the given value is not an object + */ + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { - if (!is_object($value)) { - // error + if (!is_object($object)) { + throw new UnexpectedTypeException($object, 'object'); } parent::__construct( - $value, + $object, $metadata, $propertyPath, $groups, $cascadedGroups ); } - } diff --git a/src/Symfony/Component/Validator/Node/GenericNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php index aab73db64e235..82ee9ac7fbaad 100644 --- a/src/Symfony/Component/Validator/Node/GenericNode.php +++ b/src/Symfony/Component/Validator/Node/GenericNode.php @@ -12,7 +12,14 @@ namespace Symfony\Component\Validator\Node; /** - * @since %%NextVersion%% + * Represents a value that has neither class metadata nor property metadata + * attached to it. + * + * Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, + * this node type can be used to validate a value against some given + * constraints. + * + * @since 2.5 * @author Bernhard Schussek */ class GenericNode extends Node diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 08b2e4da7835a..014665abf4ffb 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -14,21 +14,60 @@ use Symfony\Component\Validator\Mapping\MetadataInterface; /** - * @since %%NextVersion%% + * A node in the validated graph. + * + * @since 2.5 * @author Bernhard Schussek */ abstract class Node { + /** + * The validated value. + * + * @var mixed + */ public $value; + /** + * The metadata specifying how the value should be validated. + * + * @var MetadataInterface + */ public $metadata; + /** + * The property path leading to this node. + * + * @var string + */ public $propertyPath; + /** + * The groups in which the value should be validated. + * + * @var string[] + */ public $groups; + /** + * The groups in which cascaded values should be validated. + * + * @var string[] + */ public $cascadedGroups; + /** + * Creates a new property node. + * + * @param mixed $value The property value + * @param MetadataInterface $metadata The property's metadata + * @param string $propertyPath The property path leading to + * this node + * @param string[] $groups The groups in which this node + * should be validated + * @param string[] $cascadedGroups The groups in which cascaded + * objects should be validated + */ public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { $this->value = $value; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 76cfcb35312ee..bcb462b1761f5 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -14,7 +14,20 @@ use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; /** - * @since %%NextVersion%% + * Represents the value of a property and its associated metadata. + * + * If the property contains an object and should be cascaded, a new + * {@link ClassNode} instance will be created for that object. + * + * Example: + * + * (Article:ClassNode) + * \ + * (author:PropertyNode) + * \ + * (Author:ClassNode) + * + * @since 2.5 * @author Bernhard Schussek */ class PropertyNode extends Node @@ -24,6 +37,19 @@ class PropertyNode extends Node */ public $metadata; + /** + * Creates a new property node. + * + * @param mixed $value The property value + * @param PropertyMetadataInterface $metadata The property's metadata + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[] $cascadedGroups The groups in which + * cascaded objects should + * be validated + */ public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { parent::__construct( diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php new file mode 100644 index 0000000000000..1241d1bb5b830 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Node; + +use Symfony\Component\Validator\Node\ClassNode; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class ClassNodeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException + */ + public function testConstructorExpectsObject() + { + $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); + + new ClassNode('foobar', $metadata, '', array(), array()); + } +} From a3555fbd992accd92e81c43174ffd490ec69c239 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 17:20:08 +0100 Subject: [PATCH 037/323] [Validator] Fixed: Objects are not traversed unless they are instances of Traversable --- .../Component/Validator/Constraints/Valid.php | 19 +- .../Validator/Mapping/GenericMetadata.php | 3 +- .../Validator/Mapping/MemberMetadata.php | 19 ++ .../Validator/Mapping/TraversalStrategy.php | 2 + .../Component/Validator/Node/ClassNode.php | 4 +- src/Symfony/Component/Validator/Node/Node.php | 11 +- .../Component/Validator/Node/PropertyNode.php | 4 +- .../Validator/NodeTraverser/NodeTraverser.php | 283 +++++++++++++----- .../Validator/NodeVisitor/NodeValidator.php | 5 +- .../Tests/Validator/AbstractValidatorTest.php | 220 +++++++++++++- .../Tests/Validator/LegacyValidatorTest.php | 35 +++ .../Validator/Validator/AbstractValidator.php | 38 +-- .../Validator/ContextualValidator.php | 4 +- .../Validator/Validator/LegacyValidator.php | 4 +- .../Validator/Validator/Validator.php | 4 +- 15 files changed, 539 insertions(+), 116 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 6e84e9a5f053f..9f15fdb04e1db 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -21,12 +21,27 @@ * * @api */ -class Valid extends Traverse +class Valid extends Constraint { + /** + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Traverse} constraint instead. + */ + public $traverse = true; + + /** + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Traverse} constraint instead. + */ + public $deep = false; + public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { - throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint %s', __CLASS__)); + throw new ConstraintDefinitionException(sprintf( + 'The option "groups" is not supported by the constraint %s', + __CLASS__ + )); } parent::__construct($options); diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 75b07c8825ccf..369276f220e89 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -77,8 +77,7 @@ public function addConstraint(Constraint $constraint) if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; - // Continue. Valid extends Traverse, so the return statement in the - // next block is going be executed. + return $this; } if ($constraint instanceof Traverse) { diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 80a2687458ddb..230af7d89e416 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; @@ -58,6 +59,24 @@ public function addConstraint(Constraint $constraint) )); } + // BC with Symfony < 2.5 + // Only process if the traversal strategy was not already set by the + // Traverse constraint + if ($constraint instanceof Valid && !$this->traversalStrategy) { + if (true === $constraint->traverse) { + // Try to traverse cascaded objects, but ignore if they do not + // implement Traversable + $this->traversalStrategy = TraversalStrategy::TRAVERSE + | TraversalStrategy::IGNORE_NON_TRAVERSABLE; + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } + } elseif (false === $constraint->traverse) { + $this->traversalStrategy = TraversalStrategy::NONE; + } + } + parent::addConstraint($constraint); return $this; diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 4a9d8c8aa1551..951ec6058d87a 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -25,6 +25,8 @@ class TraversalStrategy const RECURSIVE = 4; + const IGNORE_NON_TRAVERSABLE = 8; + private function __construct() { } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 07554963d7feb..904e8651fdbc6 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -37,13 +37,13 @@ class ClassNode extends Node * to this node * @param string[] $groups The groups in which this * node should be validated - * @param string[] $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated * * @throws UnexpectedTypeException If the given value is not an object */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 014665abf4ffb..b301db8dda696 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; /** @@ -65,11 +66,17 @@ abstract class Node * this node * @param string[] $groups The groups in which this node * should be validated - * @param string[] $cascadedGroups The groups in which cascaded + * @param string[]|null $cascadedGroups The groups in which cascaded * objects should be validated + * + * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { + if (null !== $cascadedGroups && !is_array($cascadedGroups)) { + throw new UnexpectedTypeException($cascadedGroups, 'null or array'); + } + $this->value = $value; $this->metadata = $metadata; $this->propertyPath = $propertyPath; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index bcb462b1761f5..313da63ab7487 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -46,11 +46,11 @@ class PropertyNode extends Node * to this node * @param string[] $groups The groups in which this * node should be validated - * @param string[] $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated */ - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { parent::__construct( $value, diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index d0529af0050e9..d9cbf62c71626 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -80,100 +82,219 @@ public function traverse(array $nodes) } if ($isTopLevelCall) { - $this->traversalStarted = false; - foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ $visitor->afterTraversal($nodes); } + + $this->traversalStarted = false; } } - private function traverseNode(Node $node) + /** + * @param Node $node + * + * @return Boolean + */ + private function enterNode(Node $node) { - $stopTraversal = false; + $continueTraversal = true; foreach ($this->visitors as $visitor) { if (false === $visitor->enterNode($node)) { - $stopTraversal = true; - } - } - - // Stop the traversal, but execute the leaveNode() methods anyway to - // perform possible cleanups - if (!$stopTraversal && null !== $node->value) { - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->metadata->getTraversalStrategy(); + $continueTraversal = false; - if (is_array($node->value)) { - $this->cascadeCollection( - $node->value, - $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy - ); - } elseif ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $node->value, - $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy - ); + // Continue, so that the enterNode() method of all visitors + // is called } } + return $continueTraversal; + } + + /** + * @param Node $node + */ + private function leaveNode(Node $node) + { foreach ($this->visitors as $visitor) { $visitor->leaveNode($node); } } - private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseNode(Node $node) { - $stopTraversal = false; + $continue = $this->enterNode($node); - foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node)) { - $stopTraversal = true; - } + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $continue) { + $this->leaveNode($node); + + return; } - // Stop the traversal, but execute the leaveNode() methods anyway to - // perform possible cleanups - if (!$stopTraversal && count($node->groups) > 0) { - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $this->traverseNode(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); - } - } + if (null === $node->value) { + $this->leaveNode($node); + + return; + } + + // The "cascadedGroups" property is set by the NodeValidator when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + $this->leaveNode($node); + + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->metadata->getTraversalStrategy(); + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + + $this->leaveNode($node); + + return; + } + + if ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - $traversalStrategy = $node->metadata->getTraversalStrategy(); + $this->leaveNode($node); + + return; + } + + // Traverse only if the TRAVERSE bit is set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + $this->leaveNode($node); + + return; + } + + if (!$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { + $this->leaveNode($node); + + return; } - if ($traversalStrategy & TraversalStrategy::TRAVERSE) { - $this->cascadeCollection( - $node->value, - $node->propertyPath, - $node->groups, - $traversalStrategy - ); + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($node->value) + )); + } + + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + + $this->leaveNode($node); + } + + private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) + { + $continue = $this->enterNode($node); + + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $continue) { + $this->leaveNode($node); + + return; + } + + if (0 === count($node->groups)) { + $this->leaveNode($node); + + return; + } + + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + $this->traverseNode(new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + )); } } - foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node); + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the class' metadata + if (TraversalStrategy::IMPLICIT === $traversalStrategy) { + $traversalStrategy = $node->metadata->getTraversalStrategy(); } + + // Traverse only if the TRAVERSE bit is set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + $this->leaveNode($node); + + return; + } + + if (!$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { + $this->leaveNode($node); + + return; + } + + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($node->value) + )); + } + + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + + $this->leaveNode($node); } private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) @@ -181,24 +302,31 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal try { $classMetadata = $this->metadataFactory->getMetadataFor($object); + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + $classNode = new ClassNode( $object, $classMetadata, $propertyPath, - $groups, $groups ); $this->traverseClassNode($classNode, $traversalStrategy); } catch (NoSuchMetadataException $e) { - if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Rethrow if the TRAVERSE bit is not set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { throw $e; } - // Metadata doesn't necessarily have to exist for - // traversable objects, because we know how to validate - // them anyway. - $this->cascadeCollection( + // Rethrow if the object does not implement Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + // In that case, iterate the object and cascade each entry + $this->cascadeEachObjectIn( $object, $propertyPath, $groups, @@ -207,15 +335,25 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal } } - private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy) { - if (!($traversalStrategy & TraversalStrategy::RECURSIVE)) { + if ($traversalStrategy & TraversalStrategy::RECURSIVE) { + // Try to traverse nested objects, but ignore if they do not + // implement Traversable + $traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE; + } else { + // If the RECURSIVE bit is not set, change the strategy to IMPLICIT + // in order to respect the metadata's traversal strategy of each entry + // in the collection $traversalStrategy = TraversalStrategy::IMPLICIT; } foreach ($collection as $key => $value) { if (is_array($value)) { - $this->cascadeCollection( + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeEachObjectIn( $value, $propertyPath.'['.$key.']', $groups, @@ -226,6 +364,7 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t } // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) if (is_object($value)) { $this->cascadeObject( $value, diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 100c9b7d3b538..6ee8b7f8260db 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -146,7 +146,10 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) foreach ($groupSequence->groups as $groupInSequence) { $node = clone $node; $node->groups = array($groupInSequence); - $node->cascadedGroups = array($groupSequence->cascadedGroup ?: $groupInSequence); + + if (null !== $groupSequence->cascadedGroup) { + $node->cascadedGroups = array($groupSequence->cascadedGroup); + } $this->nodeTraverser->traverse(array($node)); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 60e5ebf76e8c2..36b13cfd96444 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; @@ -225,6 +226,45 @@ public function testArray() 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testArrayLegacyApi() + { + $test = $this; + $entity = new Entity(); + $array = array('key' => $entity); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ @@ -264,6 +304,45 @@ public function testRecursiveArray() 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testRecursiveArrayLegacyApi() + { + $test = $this; + $entity = new Entity(); + $array = array(2 => array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ @@ -278,17 +357,24 @@ public function testRecursiveArray() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testTraversableTraverseDisabled() + public function testTraversableTraverseEnabled() { $test = $this; $entity = new Entity(); $traversable = new \ArrayIterator(array('key' => $entity)); - $callback = function () use ($test) { - $test->fail('Should not be called'); + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); }; $this->metadata->addConstraint(new Callback(array( @@ -296,10 +382,21 @@ public function testTraversableTraverseDisabled() 'groups' => 'Group', ))); - $this->validator->validate($traversable, 'Group'); + $violations = $this->validator->validateCollection($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); } - public function testTraversableTraverseEnabled() + public function testTraversableTraverseEnabledLegacyApi() { $test = $this; $entity = new Entity(); @@ -338,6 +435,27 @@ public function testTraversableTraverseEnabled() $this->assertNull($violations[0]->getCode()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + /** * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException */ @@ -358,6 +476,29 @@ public function testRecursiveTraversableRecursiveTraversalDisabled() 'groups' => 'Group', ))); + $this->validator->validateCollection($traversable, 'Group'); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->validator->validate($traversable, 'Group', true); } @@ -388,6 +529,47 @@ public function testRecursiveTraversableRecursiveTraversalEnabled() 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($traversable, 'Group', true, true); /** @var ConstraintViolationInterface[] $violations */ @@ -1675,6 +1857,28 @@ public function testValidateInSeparateContext() $test->assertSame('Separate violation', $violations[0]->getMessage()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverse() + { + $entity = new Entity(); + + $this->validator->validateValue($entity, new Traverse()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverseOnClass() + { + $entity = new Entity(); + + $this->metadata->addConstraint(new Traverse()); + + $this->validator->validate($entity); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index c66cde30ea886..0d8ade455cc82 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -55,6 +55,41 @@ public function testValidateInSeparateContext() $this->markTestSkipped('Not supported in the legacy API'); } + public function testArray() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveArray() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testTraversableTraverseEnabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveTraversableRecursiveTraversalEnabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testExpectTraversableIfTraverse() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testExpectTraversableIfTraverseOnClass() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + /** * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index d4d4e62e9c976..8cd2767bd03eb 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -72,6 +72,25 @@ public function hasMetadataFor($object) return $this->metadataFactory->hasMetadataFor($object); } + protected function traverse($value, $constraints, $groups = null) + { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->nodeTraverser->traverse(array(new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups, + $groups + ))); + } + protected function traverseObject($object, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -158,25 +177,6 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups $this->nodeTraverser->traverse($nodes); } - protected function traverseValue($value, $constraints, $groups = null) - { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - protected function normalizeGroups($groups) { if (is_array($groups)) { diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index a3e21863df7b4..b975ef4bde1d3 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -58,7 +58,7 @@ public function atPath($subPath) */ public function validate($value, $constraints, $groups = null) { - $this->traverseValue($value, $constraints, $groups); + $this->traverse($value, $constraints, $groups); return $this->context->getViolations(); } @@ -89,7 +89,7 @@ public function validateCollection($collection, $groups = null, $deep = false) 'deep' => $deep, )); - $this->traverseValue($collection, $constraint, $groups); + $this->traverse($collection, $constraint, $groups); return $this->context->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 64df69e1e26da..73d6948e3ece4 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -35,7 +35,7 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals 'deep' => $deep, )); - return $this->validateValue($value, $constraint, $groups); + return parent::validate($value, $constraint, $groups); } if ($traverse && $value instanceof \Traversable) { @@ -44,7 +44,7 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals new Traverse(array('traverse' => true, 'deep' => $deep)), ); - return $this->validateValue($value, $constraints, $groups); + return parent::validate($value, $constraints, $groups); } return $this->validateObject($value, $groups); diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 5d8b509be3eb6..94857a3a1b555 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -38,7 +38,7 @@ public function validate($value, $constraints, $groups = null) { $this->contextManager->startContext($value); - $this->traverseValue($value, $constraints, $groups); + $this->traverse($value, $constraints, $groups); return $this->contextManager->stopContext()->getViolations(); } @@ -61,7 +61,7 @@ public function validateCollection($collection, $groups = null, $deep = false) 'deep' => $deep, )); - $this->traverseValue($collection, $constraint, $groups); + $this->traverse($collection, $constraint, $groups); return $this->contextManager->stopContext()->getViolations(); } From bc295919358eaf625dc5cf751c1b73fb0b4fa43d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 18:06:10 +0100 Subject: [PATCH 038/323] [Validator] Clearly separated classes supporting the API <2.5/2.5+ --- .../Validator/Context/ExecutionContext.php | 57 +- .../Context/ExecutionContextManager.php | 26 +- .../Context/LegacyExecutionContextManager.php | 36 + .../Tests/Fixtures/FakeMetadataFactory.php | 8 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 407 +++++++++ .../Tests/Validator/AbstractLegacyApiTest.php | 224 +++++ .../Tests/Validator/AbstractValidatorTest.php | 821 ++---------------- .../Validator/LegacyValidator2Dot5ApiTest.php | 47 + .../LegacyValidatorLegacyApiTest.php | 47 + .../Tests/Validator/LegacyValidatorTest.php | 100 --- .../Tests/Validator/Validator2Dot5ApiTest.php | 47 + .../Tests/Validator/ValidatorTest.php | 97 --- .../Validator/Tests/ValidatorTest.php | 36 + 13 files changed, 1025 insertions(+), 928 deletions(-) create mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/ValidatorTest.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index e2b582843f28e..43794988056b6 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -13,10 +13,14 @@ use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Exception\BadMethodCallException; +use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -30,7 +34,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface +class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface { /** * The root value of the validated object graph. @@ -151,8 +155,12 @@ public function popNode() /** * {@inheritdoc} */ - public function addViolation($message, array $parameters = array()) + public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { + // The parameters $invalidValue and following are ignored by the new + // API, as they are not present in the new interface anymore. + // You should use buildViolation() instead. + $this->violations->add(new ConstraintViolation( $this->translator->trans($message, $parameters, $this->translationDomain), $message, @@ -259,4 +267,49 @@ public function getPropertyPath($subPath = '') return PropertyPath::append($propertyPath, $subPath); } + + /** + * {@inheritdoc} + */ + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + { + throw new BadMethodCallException( + 'addViolationAt() is not supported anymore in the new API. '. + 'Please use buildViolation() or enable the legacy mode.' + ); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) + { + throw new BadMethodCallException( + 'validate() is not supported anymore in the new API. '. + 'Please use getValidator() or enable the legacy mode.' + ); + } + + /** + * {@inheritdoc} + */ + public function validateValue($value, $constraints, $subPath = '', $groups = null) + { + throw new BadMethodCallException( + 'validateValue() is not supported anymore in the new API. '. + 'Please use getValidator() or enable the legacy mode.' + ); + } + + /** + * {@inheritdoc} + */ + public function getMetadataFactory() + { + throw new BadMethodCallException( + 'getMetadataFactory() is not supported anymore in the new API. '. + 'Please use getMetadataFor() or hasMetadataFor() or enable the '. + 'legacy mode.' + ); + } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index decefdc8a881d..6214eabf46a66 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -110,13 +110,14 @@ public function startContext($root) ); } - $this->currentContext = new LegacyExecutionContext( + $this->currentContext = $this->createContext( $root, $this->validator, $this->groupManager, $this->translator, $this->translationDomain ); + $this->contextStack->push($this->currentContext); return $this->currentContext; @@ -188,4 +189,27 @@ public function leaveNode(Node $node) $this->currentContext->popNode(); } + + /** + * Creates a new context. + * + * Can be overridden by subclasses. + * + * @param mixed $root The root value of the + * validated object graph + * @param ValidatorInterface $validator The validator + * @param GroupManagerInterface $groupManager The manager for accessing + * the currently validated + * group + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain to + * use for translating + * violation messages + * + * @return ExecutionContextInterface The created context + */ + protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) + { + return new ExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); + } } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php new file mode 100644 index 0000000000000..361445bbfc080 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * A context manager that creates contexts compatible to the API < Symfony 2.5. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @see ExecutionContextManagerInterface + * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface + */ +class LegacyExecutionContextManager extends ExecutionContextManager +{ + /** + * {@inheritdoc} + */ + protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) + { + return new LegacyExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index ba39823be6e16..b03eacf71e90a 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -11,9 +11,10 @@ namespace Symfony\Component\Validator\Tests\Fixtures; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\MetadataInterface; class FakeMetadataFactory implements MetadataFactoryInterface { @@ -53,4 +54,9 @@ public function addMetadata(ClassMetadata $metadata) { $this->metadatas[$metadata->getClassName()] = $metadata; } + + public function addMetadataForValue($value, MetadataInterface $metadata) + { + $this->metadatas[$value] = $metadata; + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php new file mode 100644 index 0000000000000..48d8f07141d07 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -0,0 +1,407 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\Reference; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Verifies that a validator satisfies the API of Symfony 2.5+. + * + * @since 2.5 + * @author Bernhard Schussek + */ +abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest +{ + /** + * @var ValidatorInterface + */ + protected $validator; + + /** + * @param MetadataFactoryInterface $metadataFactory + * + * @return ValidatorInterface + */ + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + + protected function setUp() + { + parent::setUp(); + + $this->validator = $this->createValidator($this->metadataFactory); + } + + protected function validate($value, $constraints, $groups = null) + { + return $this->validator->validate($value, $constraints, $groups); + } + + protected function validateObject($object, $groups = null) + { + return $this->validator->validateObject($object, $groups); + } + + protected function validateCollection($collection, $groups = null, $deep = false) + { + return $this->validator->validateCollection($collection, $groups, $deep); + } + + protected function validateProperty($object, $propertyName, $groups = null) + { + return $this->validator->validateProperty($object, $propertyName, $groups); + } + + protected function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + return $this->validator->validatePropertyValue($object, $propertyName, $value, $groups); + } + + public function testNoDuplicateValidationIfConstraintInMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testGroupSequenceAbortsAfterFailedGroup() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 2'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); + $violations = $this->validateObject($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message 1', $violations[0]->getMessage()); + } + + public function testGroupSequenceIncludesReferences() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 2'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 1', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 2', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $violations = $this->validateObject($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Reference violation 1', $violations[0]->getMessage()); + } + + public function testValidateInSeparateContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $violations = $context + ->getValidator() + // Since the validator is not context aware, the group must + // be passed explicitly + ->validateObject($value->reference, 'Group') + ; + + /** @var ConstraintViolationInterface[] $violations */ + $test->assertCount(1, $violations); + $test->assertSame('Message value', $violations[0]->getMessage()); + $test->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $test->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $test->assertSame('', $violations[0]->getPropertyPath()); + // The root is different as we're in a new context + $test->assertSame($entity->reference, $violations[0]->getRoot()); + $test->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $test->assertNull($violations[0]->getMessagePluralization()); + $test->assertNull($violations[0]->getCode()); + + // Verify that this method is called + $context->addViolation('Separate violation'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($entity->reference, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $test->assertSame('Separate violation', $violations[0]->getMessage()); + } + + public function testValidateInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateObject($value->reference) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateCollection(array('key' => $value->reference)) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateAcceptsValid() + { + $test = $this; + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + // This is the same as when calling validateObject() + $violations = $this->validator->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverse() + { + $entity = new Entity(); + + $this->validate($entity, new Traverse()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverseOnClass() + { + $entity = new Entity(); + + $this->metadata->addConstraint(new Traverse()); + + $this->validateObject($entity); + } + + public function testAddCustomizedViolation() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->buildViolation('Message %param%') + ->setParameter('%param%', 'value') + ->setInvalidValue('Invalid value') + ->setPluralization(2) + ->setCode('Code') + ->addViolation(); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $violations = $this->validateObject($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); + $this->assertSame(2, $violations[0]->getMessagePluralization()); + $this->assertSame('Code', $violations[0]->getCode()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php new file mode 100644 index 0000000000000..4d76fa38c18aa --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\Reference; +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; + +/** + * Verifies that a validator satisfies the API of Symfony < 2.5. + * + * @since 2.5 + * @author Bernhard Schussek + */ +abstract class AbstractLegacyApiTest extends AbstractValidatorTest +{ + /** + * @var LegacyValidatorInterface + */ + protected $validator; + + /** + * @param MetadataFactoryInterface $metadataFactory + * + * @return LegacyValidatorInterface + */ + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + + protected function setUp() + { + parent::setUp(); + + $this->validator = $this->createValidator($this->metadataFactory); + } + + protected function validate($value, $constraints, $groups = null) + { + return $this->validator->validateValue($value, $constraints, $groups); + } + + protected function validateObject($object, $groups = null) + { + return $this->validator->validate($object, $groups); + } + + protected function validateCollection($collection, $groups = null, $deep = false) + { + return $this->validator->validate($collection, $groups, true, $deep); + } + + protected function validateProperty($object, $propertyName, $groups = null) + { + return $this->validator->validateProperty($object, $propertyName, $groups); + } + + protected function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + return $this->validator->validatePropertyValue($object, $propertyName, $value, $groups); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + + public function testValidateInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate($value->reference, 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate(array('key' => $value->reference), 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testAddCustomizedViolation() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation( + 'Message %param%', + array('%param%' => 'value'), + 'Invalid value', + 2, + 'Code' + ); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $violations = $this->validateObject($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); + $this->assertSame(2, $violations[0]->getMessagePluralization()); + $this->assertSame('Code', $violations[0]->getCode()); + } + + public function testGetMetadataFactory() + { + $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 36b13cfd96444..99d250fb5a587 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -11,25 +11,16 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; 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\Entity; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\ValidatorInterface; /** * @since 2.5 @@ -41,11 +32,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase const REFERENCE_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Reference'; - /** - * @var ValidatorInterface - */ - protected $validator; - /** * @var FakeMetadataFactory */ @@ -64,7 +50,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->metadataFactory = new FakeMetadataFactory(); - $this->validator = $this->createValidator($this->metadataFactory); $this->metadata = new ClassMetadata(self::ENTITY_CLASS); $this->referenceMetadata = new ClassMetadata(self::REFERENCE_CLASS); $this->metadataFactory->addMetadata($this->metadata); @@ -74,12 +59,54 @@ protected function setUp() protected function tearDown() { $this->metadataFactory = null; - $this->validator = null; $this->metadata = null; $this->referenceMetadata = null; } - abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + abstract protected function validate($value, $constraints, $groups = null); + + abstract protected function validateObject($object, $groups = null); + + abstract protected function validateCollection($collection, $groups = null, $deep = false); + + abstract protected function validateProperty($object, $propertyName, $groups = null); + + abstract protected function validatePropertyValue($object, $propertyName, $value, $groups = null); + + public function testValidate() + { + $test = $this; + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->assertNull($context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame('Bernhard', $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $constraint = new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + )); + + $violations = $this->validate('Bernhard', $constraint, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame('Bernhard', $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } public function testClassConstraint() { @@ -92,7 +119,6 @@ public function testClassConstraint() $test->assertSame('', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -105,7 +131,7 @@ public function testClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -133,7 +159,6 @@ public function testPropertyConstraint() $test->assertSame('firstName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); $test->assertSame('Bernhard', $value); @@ -146,7 +171,7 @@ public function testPropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -174,7 +199,6 @@ public function testGetterConstraint() $test->assertSame('lastName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Schussek', $context->getValue()); $test->assertSame('Schussek', $value); @@ -187,7 +211,7 @@ public function testGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -213,46 +237,6 @@ public function testArray() $test->assertSame('[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($array, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($array, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[key]', $violations[0]->getPropertyPath()); - $this->assertSame($array, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testArrayLegacyApi() - { - $test = $this; - $entity = new Entity(); - $array = array('key' => $entity); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($array, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -265,7 +249,7 @@ public function testArrayLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($array, 'Group'); + $violations = $this->validateCollection($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -291,46 +275,6 @@ public function testRecursiveArray() $test->assertSame('[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($array, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($array, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); - $this->assertSame($array, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testRecursiveArrayLegacyApi() - { - $test = $this; - $entity = new Entity(); - $array = array(2 => array('key' => $entity)); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[2][key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($array, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -343,7 +287,7 @@ public function testRecursiveArrayLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($array, 'Group'); + $violations = $this->validateCollection($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -357,46 +301,7 @@ public function testRecursiveArrayLegacyApi() $this->assertNull($violations[0]->getCode()); } - public function testTraversableTraverseEnabled() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array('key' => $entity)); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($traversable, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($traversable, 'Group', true); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[key]', $violations[0]->getPropertyPath()); - $this->assertSame($traversable, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testTraversableTraverseEnabledLegacyApi() + public function testTraversable() { $test = $this; $entity = new Entity(); @@ -408,7 +313,6 @@ public function testTraversableTraverseEnabledLegacyApi() $test->assertSame('[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($traversable, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -421,7 +325,7 @@ public function testTraversableTraverseEnabledLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($traversable, 'Group', true); + $violations = $this->validateCollection($traversable, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -435,27 +339,6 @@ public function testTraversableTraverseEnabledLegacyApi() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testTraversableTraverseDisabledLegacyApi() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array('key' => $entity)); - - $callback = function () use ($test) { - $test->fail('Should not be called'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $this->validator->validate($traversable, 'Group'); - } - /** * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException */ @@ -476,30 +359,7 @@ public function testRecursiveTraversableRecursiveTraversalDisabled() 'groups' => 'Group', ))); - $this->validator->validateCollection($traversable, 'Group'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testRecursiveTraversableRecursiveTraversalDisabledLegacyApi() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => $entity)), - )); - - $callback = function () use ($test) { - $test->fail('Should not be called'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $this->validator->validate($traversable, 'Group', true); + $this->validateCollection($traversable, 'Group'); } public function testRecursiveTraversableRecursiveTraversalEnabled() @@ -516,48 +376,6 @@ public function testRecursiveTraversableRecursiveTraversalEnabled() $test->assertSame('[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($traversable, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($traversable, 'Group', true); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); - $this->assertSame($traversable, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => $entity)), - )); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[2][key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($traversable, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -570,7 +388,7 @@ public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($traversable, 'Group', true, true); + $violations = $this->validateCollection($traversable, 'Group', true); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -596,7 +414,6 @@ public function testReferenceClassConstraint() $test->assertSame('reference', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference, $context->getValue()); $test->assertSame($entity->reference, $value); @@ -610,7 +427,7 @@ public function testReferenceClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -639,7 +456,6 @@ public function testReferencePropertyConstraint() $test->assertSame('reference.value', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Foobar', $context->getValue()); $test->assertSame('Foobar', $value); @@ -653,7 +469,7 @@ public function testReferencePropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -682,7 +498,6 @@ public function testReferenceGetterConstraint() $test->assertSame('reference.privateValue', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bamboo', $context->getValue()); $test->assertSame('Bamboo', $value); @@ -696,7 +511,7 @@ public function testReferenceGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -717,7 +532,7 @@ public function testsIgnoreNullReference() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -733,7 +548,7 @@ public function testFailOnScalarReferences() $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->validator->validate($entity); + $this->validateObject($entity); } public function testArrayReference() @@ -748,7 +563,6 @@ public function testArrayReference() $test->assertSame('reference[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference['key'], $context->getValue()); $test->assertSame($entity->reference['key'], $value); @@ -762,7 +576,7 @@ public function testArrayReference() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -789,7 +603,6 @@ public function testRecursiveArrayReference() $test->assertSame('reference[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference[2]['key'], $context->getValue()); $test->assertSame($entity->reference[2]['key'], $value); @@ -803,7 +616,7 @@ public function testRecursiveArrayReference() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -831,7 +644,7 @@ public function testArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -851,7 +664,7 @@ public function testRecursiveArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -864,7 +677,7 @@ public function testIgnoreScalarsDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -877,7 +690,7 @@ public function testIgnoreNullDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -895,7 +708,6 @@ public function testTraversableReference() $test->assertSame('reference[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference['key'], $context->getValue()); $test->assertSame($entity->reference['key'], $value); @@ -909,7 +721,7 @@ public function testTraversableReference() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -938,7 +750,7 @@ public function testDisableTraversableTraversal() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -956,7 +768,7 @@ public function testMetadataMustExistIfTraversalIsDisabled() 'traverse' => false, ))); - $this->validator->validate($entity, 'Default', ''); + $this->validateObject($entity, 'Default', ''); } public function testNoRecursiveTraversableTraversal() @@ -974,7 +786,7 @@ public function testNoRecursiveTraversableTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -994,7 +806,6 @@ public function testEnableRecursiveTraversableTraversal() $test->assertSame('reference[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference[2]['key'], $context->getValue()); $test->assertSame($entity->reference[2]['key'], $value); @@ -1010,7 +821,7 @@ public function testEnableRecursiveTraversableTraversal() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1039,7 +850,6 @@ public function testValidateProperty() $test->assertSame('firstName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); $test->assertSame('Bernhard', $value); @@ -1060,7 +870,7 @@ public function testValidateProperty() 'groups' => 'Group', ))); - $violations = $this->validator->validateProperty($entity, 'firstName', 'Group'); + $violations = $this->validateProperty($entity, 'firstName', 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1082,14 +892,9 @@ public function testValidatePropertyFailsIfPropertiesNotSupported() // $metadata does not implement PropertyMetadataContainerInterface $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); - $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); - $metadataFactory->expects($this->any()) - ->method('getMetadataFor') - ->with('VALUE') - ->will($this->returnValue($metadata)); - $validator = $this->createValidator($metadataFactory); + $this->metadataFactory->addMetadataForValue('VALUE', $metadata); - $validator->validateProperty('VALUE', 'someProperty'); + $this->validateProperty('VALUE', 'someProperty'); } public function testValidatePropertyValue() @@ -1106,7 +911,6 @@ public function testValidatePropertyValue() $test->assertSame('firstName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); $test->assertSame('Bernhard', $value); @@ -1127,7 +931,7 @@ public function testValidatePropertyValue() 'groups' => 'Group', ))); - $violations = $this->validator->validatePropertyValue( + $violations = $this->validatePropertyValue( $entity, 'firstName', 'Bernhard', @@ -1154,97 +958,26 @@ public function testValidatePropertyValueFailsIfPropertiesNotSupported() // $metadata does not implement PropertyMetadataContainerInterface $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); - $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); - $metadataFactory->expects($this->any()) - ->method('getMetadataFor') - ->with('VALUE') - ->will($this->returnValue($metadata)); - $validator = $this->createValidator($metadataFactory); + $this->metadataFactory->addMetadataForValue('VALUE', $metadata); - $validator->validatePropertyValue('VALUE', 'someProperty', 'someValue'); + $this->validatePropertyValue('VALUE', 'someProperty', 'someValue'); } - public function testValidateValue() + public function testValidateObjectOnlyOncePerGroup() { - $test = $this; - - $callback = function ($value, ExecutionContextInterface $context) use ($test) { - $test->assertNull($context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame('Bernhard', $context->getRoot()); - $test->assertSame('Bernhard', $context->getValue()); - $test->assertSame('Bernhard', $value); + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference2 = $entity->reference; - $context->addViolation('Message %param%', array('%param%' => 'value')); + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); }; - $constraint = new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - )); + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->addPropertyConstraint('reference2', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validateValue('Bernhard', $constraint, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame('Bernhard', $violations[0]->getRoot()); - $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testAddCustomizedViolation() - { - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation( - 'Message %param%', - array('%param%' => 'value'), - 'Invalid value', - 2, - 'Code' - ); - }; - - $this->metadata->addConstraint(new Callback($callback)); - - $violations = $this->validator->validate($entity); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); - $this->assertSame(2, $violations[0]->getMessagePluralization()); - $this->assertSame('Code', $violations[0]->getCode()); - } - - public function testValidateObjectOnlyOncePerGroup() - { - $entity = new Entity(); - $entity->reference = new Reference(); - $entity->reference2 = $entity->reference; - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message'); - }; - - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->metadata->addPropertyConstraint('reference2', new Valid()); - $this->referenceMetadata->addConstraint(new Callback($callback)); - - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1264,7 +997,7 @@ public function testValidateDifferentObjectsSeparately() $this->metadata->addPropertyConstraint('reference2', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); @@ -1287,7 +1020,7 @@ public function testValidateSingleGroup() 'groups' => 'Group 2', ))); - $violations = $this->validator->validate($entity, 'Group 2'); + $violations = $this->validateObject($entity, 'Group 2'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1310,93 +1043,12 @@ public function testValidateMultipleGroups() 'groups' => 'Group 2', ))); - $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); + $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); } - public function testNoDuplicateValidationIfConstraintInMultipleGroups() - { - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => array('Group 1', 'Group 2'), - ))); - - $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - } - - public function testGroupSequenceAbortsAfterFailedGroup() - { - $entity = new Entity(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message 1'); - }; - $callback2 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message 2'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => function () {}, - 'groups' => 'Group 1', - ))); - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group 2', - ))); - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group 3', - ))); - - $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); - $violations = $this->validator->validate($entity, $sequence); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message 1', $violations[0]->getMessage()); - } - - public function testGroupSequenceIncludesReferences() - { - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Reference violation 1'); - }; - $callback2 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Reference violation 2'); - }; - - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group 1', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group 2', - ))); - - $sequence = new GroupSequence(array('Group 1', 'Entity')); - $violations = $this->validator->validate($entity, $sequence); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Reference violation 1', $violations[0]->getMessage()); - } - public function testReplaceDefaultGroupByGroupSequenceObject() { $entity = new Entity(); @@ -1424,7 +1076,7 @@ public function testReplaceDefaultGroupByGroupSequenceObject() $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1458,7 +1110,7 @@ public function testReplaceDefaultGroupByGroupSequenceArray() $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1490,7 +1142,7 @@ public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1520,7 +1172,7 @@ public function testValidateCustomGroupWhenDefaultGroupWasReplaced() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Other Group'); + $violations = $this->validateObject($entity, 'Other Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1556,7 +1208,7 @@ public function testReplaceDefaultGroupWithObjectFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1592,295 +1244,10 @@ public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); } - - public function testValidateInContext() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context - ->getValidator() - ->inContext($context) - ->atPath('subpath') - ->validateObject($value->reference) - ; - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateInContextLegacyApi() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->validate($value->reference, 'subpath'); - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateArrayInContext() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context - ->getValidator() - ->inContext($context) - ->atPath('subpath') - ->validateCollection(array('key' => $value->reference)) - ; - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateArrayInContextLegacyApi() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->validate(array('key' => $value->reference), 'subpath'); - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateInSeparateContext() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $violations = $context - ->getValidator() - // Since the validator is not context aware, the group must - // be passed explicitly - ->validateObject($value->reference, 'Group') - ; - - /** @var ConstraintViolationInterface[] $violations */ - $test->assertCount(1, $violations); - $test->assertSame('Message value', $violations[0]->getMessage()); - $test->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $test->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $test->assertSame('', $violations[0]->getPropertyPath()); - // The root is different as we're in a new context - $test->assertSame($entity->reference, $violations[0]->getRoot()); - $test->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $test->assertNull($violations[0]->getMessagePluralization()); - $test->assertNull($violations[0]->getCode()); - - // Verify that this method is called - $context->addViolation('Separate violation'); - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity->reference, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $test->assertSame('Separate violation', $violations[0]->getMessage()); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ - public function testExpectTraversableIfTraverse() - { - $entity = new Entity(); - - $this->validator->validateValue($entity, new Traverse()); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ - public function testExpectTraversableIfTraverseOnClass() - { - $entity = new Entity(); - - $this->metadata->addConstraint(new Traverse()); - - $this->validator->validate($entity); - } - - public function testGetMetadataFactory() - { - $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); - } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php new file mode 100644 index 0000000000000..e3715731ce081 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Context\LegacyExecutionContextManager; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; +use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\Validator\LegacyValidator; + +class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); + + // The context manager needs the validator for passing it to created + // contexts + $contextManager->initialize($validator); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); + + $nodeTraverser->addVisitor($groupSequenceResolver); + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php new file mode 100644 index 0000000000000..c106d765c12c3 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Context\LegacyExecutionContextManager; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; +use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\Validator\LegacyValidator; + +class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); + + // The context manager needs the validator for passing it to created + // contexts + $contextManager->initialize($validator); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); + + $nodeTraverser->addVisitor($groupSequenceResolver); + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php deleted file mode 100644 index 0d8ade455cc82..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ /dev/null @@ -1,100 +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\Validator; - -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Validator as LegacyValidator; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; - -class LegacyValidatorTest extends AbstractValidatorTest -{ - protected function createValidator(MetadataFactoryInterface $metadataFactory) - { - return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - } - - public function testNoDuplicateValidationIfConstraintInMultipleGroups() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testGroupSequenceAbortsAfterFailedGroup() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testGroupSequenceIncludesReferences() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testValidateInContext() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testValidateArrayInContext() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testValidateInSeparateContext() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testArray() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testRecursiveArray() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testTraversableTraverseEnabled() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testRecursiveTraversableRecursiveTraversalDisabled() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testRecursiveTraversableRecursiveTraversalEnabled() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testExpectTraversableIfTraverse() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testExpectTraversableIfTraverseOnClass() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidateValueRejectsValid() - { - $this->validator->validateValue(new Entity(), new Valid()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php new file mode 100644 index 0000000000000..2bf4693dde361 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Context\ExecutionContextManager; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; +use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\Validator\Validator; + +class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); + $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); + + // The context manager needs the validator for passing it to created + // contexts + $contextManager->initialize($validator); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); + + $nodeTraverser->addVisitor($groupSequenceResolver); + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php deleted file mode 100644 index 2b0d00205e887..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php +++ /dev/null @@ -1,97 +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\Validator; - -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\ExecutionContextManager; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Validator\LegacyValidator; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -class ValidatorTest extends AbstractValidatorTest -{ - /** - * @var ValidatorInterface - */ - protected $validator; - - protected function createValidator(MetadataFactoryInterface $metadataFactory) - { - $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); - $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); - - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); - $nodeTraverser->addVisitor($nodeValidator); - - return $validator; - } - - public function testValidateAcceptsValid() - { - $test = $this; - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - // This is the same as when calling validateObject() - $violations = $this->validator->validate($entity, new Valid(), 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php new file mode 100644 index 0000000000000..a983a78a70819 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -0,0 +1,36 @@ + + * + * 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\Constraints\Valid; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Validator\AbstractLegacyApiTest; +use Symfony\Component\Validator\Validator as LegacyValidator; +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; + +class ValidatorTest extends AbstractLegacyApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidateValueRejectsValid() + { + $this->validator->validateValue(new Entity(), new Valid()); + } +} From 26eafa43f7e601ecf2a7454db2e0bce65f5fa82c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 18:16:34 +0100 Subject: [PATCH 039/323] [Validator] Removed unused use statements --- src/Symfony/Component/Validator/Context/ExecutionContext.php | 2 -- src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 43794988056b6..f96370288ca4e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -13,14 +13,12 @@ use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index d9cbf62c71626..eacfd85c2c994 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\NodeTraverser; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; From df41974f31e8a4c14a4c4f1bd82ace0cf5cf755c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:10:51 +0100 Subject: [PATCH 040/323] [Validator] Changed context manager to context factory The current context is not stored anymore. Instead, it is passed around the traverser and the visitors. For this reason, validation can occur in multiple contexts at the same time. --- .../Validator/Context/ExecutionContext.php | 50 ++-- .../Context/ExecutionContextFactory.php | 72 ++++++ .../ExecutionContextFactoryInterface.php | 37 +++ .../Context/ExecutionContextManager.php | 215 ------------------ .../ExecutionContextManagerInterface.php | 86 ------- .../Context/LegacyExecutionContext.php | 13 +- .../Context/LegacyExecutionContextFactory.php | 75 ++++++ .../Context/LegacyExecutionContextManager.php | 36 --- .../Validator/NodeTraverser/NodeTraverser.php | 114 +++++----- .../NodeTraverser/NodeTraverserInterface.php | 7 +- .../Validator/NodeVisitor/AbstractVisitor.php | 9 +- .../NodeVisitor/ContextRefresher.php | 60 +++++ .../NodeVisitor/GroupSequenceResolver.php | 3 +- .../Validator/NodeVisitor/NodeValidator.php | 38 +--- .../NodeVisitor/NodeVisitorInterface.php | 9 +- .../NodeVisitor/ObjectInitializer.php | 3 +- .../Context/ExecutionContextManagerTest.php | 95 -------- .../Tests/Context/ExecutionContextTest.php | 6 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 18 +- .../Validator/LegacyValidator2Dot5ApiTest.php | 18 +- .../LegacyValidatorLegacyApiTest.php | 18 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 18 +- .../Validator/Validator/AbstractValidator.php | 188 --------------- .../Validator/ContextualValidator.php | 199 ++++++++++------ .../ContextualValidatorInterface.php | 70 +++++- .../Validator/Validator/Validator.php | 109 ++++++--- .../Validator/ValidatorInterface.php | 5 + 27 files changed, 667 insertions(+), 904 deletions(-) create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextFactory.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php delete mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManager.php delete mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php create mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php delete mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php create mode 100644 src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php delete mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php delete mode 100644 src/Symfony/Component/Validator/Validator/AbstractValidator.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index f96370288ca4e..187de382fb84e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -25,7 +25,7 @@ use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; /** - * The context used and created by {@link ExecutionContextManager}. + * The context used and created by {@link ExecutionContextFactory}. * * @since 2.5 * @author Bernhard Schussek @@ -34,6 +34,11 @@ */ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface { + /** + * @var ValidatorInterface + */ + private $validator; + /** * The root value of the validated object graph. * @@ -41,6 +46,21 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont */ private $root; + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string + */ + private $translationDomain; + /** * The violations generated in the current context. * @@ -62,32 +82,12 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont */ private $nodeStack; - /** - * @var ValidatorInterface - */ - private $validator; - - /** - * @var GroupManagerInterface - */ - private $groupManager; - - /** - * @var TranslatorInterface - */ - private $translator; - - /** - * @var string - */ - private $translationDomain; - /** * Creates a new execution context. * + * @param ValidatorInterface $validator The validator * @param mixed $root The root value of the * validated object graph - * @param ValidatorInterface $validator The validator * @param GroupManagerInterface $groupManager The manager for accessing * the currently validated * group @@ -96,13 +96,13 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont * use for translating * violation messages * - * @internal Called by {@link ExecutionContextManager}. Should not be used + * @internal Called by {@link ExecutionContextFactory}. Should not be used * in user code. */ - public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { - $this->root = $root; $this->validator = $validator; + $this->root = $root; $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php new file mode 100644 index 0000000000000..3305e1a943452 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Creates new {@link ExecutionContext} instances. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class ExecutionContextFactory implements ExecutionContextFactoryInterface +{ + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string|null + */ + private $translationDomain; + + /** + * Creates a new context factory. + * + * @param GroupManagerInterface $groupManager The manager for accessing + * the currently validated + * group + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain to + * use for translating + * violation messages + */ + public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + { + $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * {@inheritdoc} + */ + public function createContext(ValidatorInterface $validator, $root) + { + return new ExecutionContext( + $validator, + $root, + $this->groupManager, + $this->translator, + $this->translationDomain + ); + } +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php new file mode 100644 index 0000000000000..f0ee00174f7c3 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Creates instances of {@link ExecutionContextInterface}. + * + * You can use a custom factory if you want to customize the execution context + * that is passed through the validation run. + * + * @since 2.5 + * @author Bernhard Schussek + */ +interface ExecutionContextFactoryInterface +{ + /** + * Creates a new execution context. + * + * @param ValidatorInterface $validator The validator + * @param mixed $root The root value of the validated + * object graph + * + * @return ExecutionContextInterface The new execution context + */ + public function createContext(ValidatorInterface $validator, $root); +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php deleted file mode 100644 index 6214eabf46a66..0000000000000 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ /dev/null @@ -1,215 +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\Context; - -use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Exception\RuntimeException; -use Symfony\Component\Validator\Group\GroupManagerInterface; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -/** - * The default implementation of {@link ExecutionContextManagerInterface}. - * - * This class implements {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface} - * and updates the current context with the current node of the validation - * traversal. - * - * After creating a new instance, the method {@link initialize()} must be - * called with a {@link ValidatorInterface} instance. Calling methods such as - * {@link startContext()} or {@link enterNode()} without initializing the - * manager first will lead to errors. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see ExecutionContextManagerInterface - * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface - */ -class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface -{ - /** - * @var GroupManagerInterface - */ - private $groupManager; - - /** - * @var ValidatorInterface - */ - private $validator; - - /** - * @var ExecutionContext - */ - private $currentContext; - - /** - * @var \SplStack|ExecutionContext[] - */ - private $contextStack; - - /** - * @var TranslatorInterface - */ - private $translator; - - /** - * @var string|null - */ - private $translationDomain; - - /** - * Creates a new context manager. - * - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group - * @param TranslatorInterface $translator The translator - * @param string|null $translationDomain The translation domain to - * use for translating - * violation messages - */ - public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) - { - $this->groupManager = $groupManager; - $this->translator = $translator; - $this->translationDomain = $translationDomain; - $this->contextStack = new \SplStack(); - } - - /** - * Initializes the manager with a validator. - * - * @param ValidatorInterface $validator The validator - */ - public function initialize(ValidatorInterface $validator) - { - $this->validator = $validator; - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ - public function startContext($root) - { - if (null === $this->validator) { - throw new RuntimeException( - 'initialize() must be called before startContext().' - ); - } - - $this->currentContext = $this->createContext( - $root, - $this->validator, - $this->groupManager, - $this->translator, - $this->translationDomain - ); - - $this->contextStack->push($this->currentContext); - - return $this->currentContext; - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link startContext()} wasn't called - */ - public function stopContext() - { - if (0 === count($this->contextStack)) { - throw new RuntimeException( - 'No context was started yet. Call startContext() before '. - 'stopContext().' - ); - } - - // Remove the current context from the stack - $stoppedContext = $this->contextStack->pop(); - - // Adjust the current context to the previous context - $this->currentContext = count($this->contextStack) > 0 - ? $this->contextStack->top() - : null; - - return $stoppedContext; - } - - /** - * {@inheritdoc} - */ - public function getCurrentContext() - { - return $this->currentContext; - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ - public function enterNode(Node $node) - { - if (null === $this->currentContext) { - throw new RuntimeException( - 'No context was started yet. Call startContext() before '. - 'enterNode().' - ); - } - - $this->currentContext->pushNode($node); - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ - public function leaveNode(Node $node) - { - if (null === $this->currentContext) { - throw new RuntimeException( - 'No context was started yet. Call startContext() before '. - 'leaveNode().' - ); - } - - $this->currentContext->popNode(); - } - - /** - * Creates a new context. - * - * Can be overridden by subclasses. - * - * @param mixed $root The root value of the - * validated object graph - * @param ValidatorInterface $validator The validator - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group - * @param TranslatorInterface $translator The translator - * @param string|null $translationDomain The translation domain to - * use for translating - * violation messages - * - * @return ExecutionContextInterface The created context - */ - protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) - { - return new ExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); - } -} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php deleted file mode 100644 index a2dd04d7dccd5..0000000000000 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ /dev/null @@ -1,86 +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\Context; - -/** - * Manages the creation and deletion of {@link ExecutionContextInterface} - * instances. - * - * Start a new context with {@link startContext()}. You can retrieve the context - * with {@link getCurrentContext()} and stop it again with {@link stopContext()}. - * - * $contextManager->startContext(); - * $context = $contextManager->getCurrentContext(); - * $contextManager->stopContext(); - * - * You can also start several nested contexts. The {@link getCurrentContext()} - * method will always return the most recently started context. - * - * // Start context 1 - * $contextManager->startContext(); - * - * // Start context 2 - * $contextManager->startContext(); - * - * // Returns context 2 - * $context = $contextManager->getCurrentContext(); - * - * // Stop context 2 - * $contextManager->stopContext(); - * - * // Returns context 1 - * $context = $contextManager->getCurrentContext(); - * - * See also {@link ExecutionContextInterface} for more information. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see ExecutionContextInterface - */ -interface ExecutionContextManagerInterface -{ - /** - * Starts a new context. - * - * The newly started context is returned. You can subsequently access the - * context with {@link getCurrentContext()}. - * - * @param mixed $root The root value of the object graph in the new context - * - * @return ExecutionContextInterface The started context - */ - public function startContext($root); - - /** - * Stops the current context. - * - * If multiple contexts have been started, the most recently started context - * is stopped. The stopped context is returned from this method. - * - * After calling this method, {@link getCurrentContext()} will return the - * context that was started before the stopped context. - * - * @return ExecutionContextInterface The stopped context - */ - public function stopContext(); - - /** - * Returns the current context. - * - * If multiple contexts have been started, the current context refers to the - * most recently started context. - * - * @return ExecutionContextInterface The current context - */ - public function getCurrentContext(); -} diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 685f06931bb61..2d85ab7905a19 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -39,8 +39,11 @@ class LegacyExecutionContext extends ExecutionContext implements LegacyExecution * it does not, an {@link InvalidArgumentException} is thrown. * * @see ExecutionContext::__construct() + * + * @internal Called by {@link LegacyExecutionContextFactory}. Should not be used + * in user code. */ - public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { if (!$validator instanceof LegacyValidatorInterface) { throw new InvalidArgumentException( @@ -49,7 +52,13 @@ public function __construct($root, ValidatorInterface $validator, GroupManagerIn ); } - parent::__construct($root, $validator, $groupManager, $translator, $translationDomain); + parent::__construct( + $validator, + $root, + $groupManager, + $translator, + $translationDomain + ); } /** diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php new file mode 100644 index 0000000000000..181b4f47fd00d --- /dev/null +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Creates new {@link LegacyExecutionContext} instances. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be + * removed in 3.0. + */ +class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface +{ + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string|null + */ + private $translationDomain; + + /** + * Creates a new context factory. + * + * @param GroupManagerInterface $groupManager The manager for accessing + * the currently validated + * group + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain to + * use for translating + * violation messages + */ + public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + { + $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * {@inheritdoc} + */ + public function createContext(ValidatorInterface $validator, $root) + { + return new LegacyExecutionContext( + $validator, + $root, + $this->groupManager, + $this->translator, + $this->translationDomain + ); + } +} diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php deleted file mode 100644 index 361445bbfc080..0000000000000 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php +++ /dev/null @@ -1,36 +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\Context; - -use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -/** - * A context manager that creates contexts compatible to the API < Symfony 2.5. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see ExecutionContextManagerInterface - * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface - */ -class LegacyExecutionContextManager extends ExecutionContextManager -{ - /** - * {@inheritdoc} - */ - protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) - { - return new LegacyExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); - } -} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index eacfd85c2c994..9d6f4d639496f 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeTraverser; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; @@ -59,7 +60,7 @@ public function removeVisitor(NodeVisitorInterface $visitor) /** * {@inheritdoc} */ - public function traverse(array $nodes) + public function traverse(array $nodes, ExecutionContextInterface $context) { $isTopLevelCall = !$this->traversalStarted; @@ -68,39 +69,34 @@ public function traverse(array $nodes) foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ - $visitor->beforeTraversal($nodes); + $visitor->beforeTraversal($nodes, $context); } } foreach ($nodes as $node) { if ($node instanceof ClassNode) { - $this->traverseClassNode($node); + $this->traverseClassNode($node, $context); } else { - $this->traverseNode($node); + $this->traverseNode($node, $context); } } if ($isTopLevelCall) { foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ - $visitor->afterTraversal($nodes); + $visitor->afterTraversal($nodes, $context); } $this->traversalStarted = false; } } - /** - * @param Node $node - * - * @return Boolean - */ - private function enterNode(Node $node) + private function enterNode(Node $node, ExecutionContextInterface $context) { $continueTraversal = true; foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node)) { + if (false === $visitor->enterNode($node, $context)) { $continueTraversal = false; // Continue, so that the enterNode() method of all visitors @@ -111,19 +107,16 @@ private function enterNode(Node $node) return $continueTraversal; } - /** - * @param Node $node - */ - private function leaveNode(Node $node) + private function leaveNode(Node $node, ExecutionContextInterface $context) { foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node); + $visitor->leaveNode($node, $context); } } - private function traverseNode(Node $node) + private function traverseNode(Node $node, ExecutionContextInterface $context) { - $continue = $this->enterNode($node); + $continue = $this->enterNode($node, $context); // Visitors have two possibilities to influence the traversal: // @@ -133,13 +126,13 @@ private function traverseNode(Node $node) // that group will be skipped in the subtree of that node. if (false === $continue) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (null === $node->value) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -151,7 +144,7 @@ private function traverseNode(Node $node) : $node->groups; if (0 === count($cascadedGroups)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -166,11 +159,12 @@ private function traverseNode(Node $node) $this->cascadeEachObjectIn( $node->value, $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy + $cascadedGroups, + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -182,25 +176,26 @@ private function traverseNode(Node $node) $this->cascadeObject( $node->value, $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy + $cascadedGroups, + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -215,16 +210,17 @@ private function traverseNode(Node $node) $this->cascadeEachObjectIn( $node->value, $node->propertyPath, - $node->groups, - $traversalStrategy + $cascadedGroups, + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); } - private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, $traversalStrategy = TraversalStrategy::IMPLICIT) { - $continue = $this->enterNode($node); + $continue = $this->enterNode($node, $context); // Visitors have two possibilities to influence the traversal: // @@ -234,28 +230,30 @@ private function traverseClassNode(ClassNode $node, $traversalStrategy = Travers // that group will be skipped in the subtree of that node. if (false === $continue) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (0 === count($node->groups)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $this->traverseNode(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); + $propertyNode = new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + ); + + $this->traverseNode($propertyNode, $context); } } @@ -267,14 +265,14 @@ private function traverseClassNode(ClassNode $node, $traversalStrategy = Travers // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -290,13 +288,14 @@ private function traverseClassNode(ClassNode $node, $traversalStrategy = Travers $node->value, $node->propertyPath, $node->groups, - $traversalStrategy + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -312,7 +311,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $groups ); - $this->traverseClassNode($classNode, $traversalStrategy); + $this->traverseClassNode($classNode, $context, $traversalStrategy); } catch (NoSuchMetadataException $e) { // Rethrow if the TRAVERSE bit is not set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { @@ -329,12 +328,13 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $object, $propertyPath, $groups, - $traversalStrategy + $traversalStrategy, + $context ); } } - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { if ($traversalStrategy & TraversalStrategy::RECURSIVE) { // Try to traverse nested objects, but ignore if they do not @@ -356,7 +356,8 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $value, $propertyPath.'['.$key.']', $groups, - $traversalStrategy + $traversalStrategy, + $context ); continue; @@ -369,7 +370,8 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $value, $propertyPath.'['.$key.']', $groups, - $traversalStrategy + $traversalStrategy, + $context ); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index d9ce42f025a8b..6084403d9bb16 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Validator\NodeTraverser; -use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** @@ -24,8 +24,5 @@ public function addVisitor(NodeVisitorInterface $visitor); public function removeVisitor(NodeVisitorInterface $visitor); - /** - * @param Node[] $nodes - */ - public function traverse(array $nodes); + public function traverse(array $nodes, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index 31b49250eb8c9..47631556f0d67 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\Node; /** @@ -19,19 +20,19 @@ */ abstract class AbstractVisitor implements NodeVisitorInterface { - public function beforeTraversal(array $nodes) + public function beforeTraversal(array $nodes, ExecutionContextInterface $context) { } - public function afterTraversal(array $nodes) + public function afterTraversal(array $nodes, ExecutionContextInterface $context) { } - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { } - public function leaveNode(Node $node) + public function leaveNode(Node $node, ExecutionContextInterface $context) { } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php new file mode 100644 index 0000000000000..e647e7c1adfe1 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeVisitor; + +use Symfony\Component\Validator\Context\ExecutionContext; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\RuntimeException; +use Symfony\Component\Validator\Node\Node; + +/** + * Updates the current context with the current node of the validation + * traversal. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class ContextRefresher extends AbstractVisitor +{ + public function enterNode(Node $node, ExecutionContextInterface $context) + { + if (!$context instanceof ExecutionContext) { + throw new RuntimeException(sprintf( + 'The ContextRefresher only supports instances of class '. + '"Symfony\Component\Validator\Context\ExecutionContext". '. + 'An instance of class "%s" was given.', + get_class($context) + )); + } + + $context->pushNode($node); + } + + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ + public function leaveNode(Node $node, ExecutionContextInterface $context) + { + if (!$context instanceof ExecutionContext) { + throw new RuntimeException(sprintf( + 'The ContextRefresher only supports instances of class '. + '"Symfony\Component\Validator\Context\ExecutionContext". '. + 'An instance of class "%s" was given.', + get_class($context) + )); + } + + $context->popNode(); + } +} diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php index 868d6fc428749..b5887ba4b0db0 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -22,7 +23,7 @@ */ class GroupSequenceResolver extends AbstractVisitor { - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 6ee8b7f8260db..e410cb259e02e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -35,11 +35,6 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface */ private $validatorFactory; - /** - * @var ExecutionContextManagerInterface - */ - private $contextManager; - /** * @var NodeTraverserInterface */ @@ -56,19 +51,14 @@ public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintVal $this->objectHashStack = new \SplStack(); } - public function initialize(ExecutionContextManagerInterface $contextManager) - { - $this->contextManager = $contextManager; - } - - public function afterTraversal(array $nodes) + public function afterTraversal(array $nodes, ExecutionContextInterface $context) { $this->validatedObjects = array(); $this->validatedConstraints = array(); $this->objectHashStack = new \SplStack(); } - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); @@ -105,7 +95,7 @@ public function enterNode(Node $node) // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($objectHash, $node, $group); + $this->validateNodeForGroup($objectHash, $node, $group, $context); continue; } @@ -114,7 +104,7 @@ public function enterNode(Node $node) unset($node->groups[$key]); // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group); + $this->traverseGroupSequence($node, $group, $context); // Optimization: If the groups only contain the group sequence, // we can skip the traversal for the properties of the object @@ -126,7 +116,7 @@ public function enterNode(Node $node) return true; } - public function leaveNode(Node $node) + public function leaveNode(Node $node, ExecutionContextInterface $context) { if ($node instanceof ClassNode) { $this->objectHashStack->pop(); @@ -138,9 +128,8 @@ public function getCurrentGroup() return $this->currentGroup; } - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) { - $context = $this->contextManager->getCurrentContext(); $violationCount = count($context->getViolations()); foreach ($groupSequence->groups as $groupInSequence) { @@ -151,7 +140,7 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) $node->cascadedGroups = array($groupSequence->cascadedGroup); } - $this->nodeTraverser->traverse(array($node)); + $this->nodeTraverser->traverse(array($node), $context); // Abort sequence validation if a violation was generated if (count($context->getViolations()) > $violationCount) { @@ -160,14 +149,7 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) } } - /** - * @param $objectHash - * @param Node $node - * @param $group - * - * @throws \Exception - */ - private function validateNodeForGroup($objectHash, Node $node, $group) + private function validateNodeForGroup($objectHash, Node $node, $group, ExecutionContextInterface $context) { try { $this->currentGroup = $group; @@ -187,7 +169,7 @@ private function validateNodeForGroup($objectHash, Node $node, $group) } $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($this->contextManager->getCurrentContext()); + $validator->initialize($context); $validator->validate($node->value, $constraint); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index a7542d515a73f..6736cc23ba966 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\Node; /** @@ -19,11 +20,11 @@ */ interface NodeVisitorInterface { - public function beforeTraversal(array $nodes); + public function beforeTraversal(array $nodes, ExecutionContextInterface $context); - public function afterTraversal(array $nodes); + public function afterTraversal(array $nodes, ExecutionContextInterface $context); - public function enterNode(Node $node); + public function enterNode(Node $node, ExecutionContextInterface $context); - public function leaveNode(Node $node); + public function leaveNode(Node $node, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php index bd000366c518e..8d44d39ee0be7 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\ObjectInitializerInterface; @@ -42,7 +43,7 @@ public function __construct(array $initializers) $this->initializers = $initializers; } - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php deleted file mode 100644 index 548a11170d57d..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php +++ /dev/null @@ -1,95 +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\Context; - -use Symfony\Component\Validator\Context\ExecutionContextManager; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class ExecutionContextManagerTest extends \PHPUnit_Framework_TestCase -{ - const TRANSLATION_DOMAIN = '__TRANSLATION_DOMAIN__'; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $validator; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $groupManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $translator; - - /** - * @var ExecutionContextManager - */ - private $contextManager; - - protected function setUp() - { - $this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface'); - $this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface'); - $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); - $this->contextManager = new ExecutionContextManager( - $this->groupManager, - $this->translator, - self::TRANSLATION_DOMAIN - ); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testInitializeMustBeCalledBeforeStartContext() - { - $this->contextManager->startContext('root'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testCannotStopContextIfNoneWasStarted() - { - $this->contextManager->stopContext(); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testCannotEnterNodeWithoutActiveContext() - { - $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') - ->disableOriginalConstructor() - ->getMock(); - - $this->contextManager->enterNode($node); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testCannotLeaveNodeWithoutActiveContext() - { - $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') - ->disableOriginalConstructor() - ->getMock(); - - $this->contextManager->leaveNode($node); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 1133f413a7cd6..8019aecea1ae4 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -51,11 +51,7 @@ protected function setUp() $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); $this->context = new ExecutionContext( - self::ROOT, - $this->validator, - $this->groupManager, - $this->translator, - self::TRANSLATION_DOMAIN + $this->validator, self::ROOT, $this->groupManager, $this->translator, self::TRANSLATION_DOMAIN ); } diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 48d8f07141d07..ad529f3750e92 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -87,7 +87,7 @@ public function testNoDuplicateValidationIfConstraintInMultipleGroups() 'groups' => array('Group 1', 'Group 2'), ))); - $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); + $violations = $this->validator->validateObject($entity, array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -118,7 +118,7 @@ public function testGroupSequenceAbortsAfterFailedGroup() ))); $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); - $violations = $this->validateObject($entity, $sequence); + $violations = $this->validator->validateObject($entity, $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -148,7 +148,7 @@ public function testGroupSequenceIncludesReferences() ))); $sequence = new GroupSequence(array('Group 1', 'Entity')); - $violations = $this->validateObject($entity, $sequence); + $violations = $this->validator->validateObject($entity, $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -207,7 +207,7 @@ public function testValidateInSeparateContext() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validator->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -251,7 +251,7 @@ public function testValidateInContext() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validator->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -302,7 +302,7 @@ public function testValidateArrayInContext() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validator->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -361,7 +361,7 @@ public function testExpectTraversableIfTraverse() { $entity = new Entity(); - $this->validate($entity, new Traverse()); + $this->validator->validate($entity, new Traverse()); } /** @@ -373,7 +373,7 @@ public function testExpectTraversableIfTraverseOnClass() $this->metadata->addConstraint(new Traverse()); - $this->validateObject($entity); + $this->validator->validateObject($entity); } public function testAddCustomizedViolation() @@ -391,7 +391,7 @@ public function testAddCustomizedViolation() $this->metadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validator->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index e3715731ce081..1cecafdedf9e0 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -13,8 +13,9 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\LegacyExecutionContextManager; +use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextRefresher; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; @@ -26,20 +27,13 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); + $contextRefresher = new ContextRefresher(); $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($contextRefresher); $nodeTraverser->addVisitor($nodeValidator); return $validator; diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index c106d765c12c3..94900e4497f52 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -13,8 +13,9 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\LegacyExecutionContextManager; +use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextRefresher; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; @@ -26,20 +27,13 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); + $contextRefresher = new ContextRefresher(); $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($contextRefresher); $nodeTraverser->addVisitor($nodeValidator); return $validator; diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index 2bf4693dde361..4d58f0a13e4c8 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -13,8 +13,9 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\ExecutionContextManager; +use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextRefresher; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; @@ -26,20 +27,13 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); + $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); + $contextRefresher = new ContextRefresher(); $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($contextRefresher); $nodeTraverser->addVisitor($nodeValidator); return $validator; diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php deleted file mode 100644 index 8cd2767bd03eb..0000000000000 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ /dev/null @@ -1,188 +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\Validator; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\Node\GenericNode; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; -use Symfony\Component\Validator\Util\PropertyPath; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -abstract class AbstractValidator implements ValidatorInterface -{ - /** - * @var NodeTraverserInterface - */ - protected $nodeTraverser; - - /** - * @var MetadataFactoryInterface - */ - protected $metadataFactory; - - /** - * @var string - */ - protected $defaultPropertyPath = ''; - - protected $defaultGroups = array(Constraint::DEFAULT_GROUP); - - public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) - { - $this->nodeTraverser = $nodeTraverser; - $this->metadataFactory = $metadataFactory; - } - - /** - * @param ExecutionContextInterface $context - * - * @return ContextualValidatorInterface - */ - public function inContext(ExecutionContextInterface $context) - { - return new ContextualValidator($this->nodeTraverser, $this->metadataFactory, $context); - } - - public function getMetadataFor($object) - { - return $this->metadataFactory->getMetadataFor($object); - } - - public function hasMetadataFor($object) - { - return $this->metadataFactory->hasMetadataFor($object); - } - - protected function traverse($value, $constraints, $groups = null) - { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - - protected function traverseObject($object, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new ClassNode( - $object, - $classMetadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - - protected function traverseProperty($object, $propertyName, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($object); - - $nodes[] = new PropertyNode( - $propertyValue, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups, - $groups - ); - } - - $this->nodeTraverser->traverse($nodes); - } - - protected function traversePropertyValue($object, $propertyName, $value, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $nodes[] = new PropertyNode( - $value, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups, - $groups - ); - } - - $this->nodeTraverser->traverse($nodes); - } - - protected function normalizeGroups($groups) - { - if (is_array($groups)) { - return $groups; - } - - return array($groups); - } -} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index b975ef4bde1d3..cb9951bf771da 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -13,30 +13,45 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; -use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Exception\ValidatorException; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\GenericNode; +use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\Util\PropertyPath; /** * @since %%NextVersion%% * @author Bernhard Schussek */ -class ContextualValidator extends AbstractValidator implements ContextualValidatorInterface +class ContextualValidator implements ContextualValidatorInterface { /** - * @var ExecutionContextManagerInterface + * @var ExecutionContextInterface */ private $context; - public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextInterface $context) - { - parent::__construct($nodeTraverser, $metadataFactory); + /** + * @var NodeTraverserInterface + */ + private $nodeTraverser; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) + { $this->context = $context; $this->defaultPropertyPath = $context->getPropertyPath(); - $this->defaultGroups = array($context->getGroup()); + $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); + $this->nodeTraverser = $nodeTraverser; + $this->metadataFactory = $metadataFactory; } public function atPath($subPath) @@ -46,40 +61,53 @@ public function atPath($subPath) return $this; } - /** - * Validates a value against a constraint or a list of constraints. - * - * @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 ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ public function validate($value, $constraints, $groups = null) { - $this->traverse($value, $constraints, $groups); + if (!is_array($constraints)) { + $constraints = array($constraints); + } - return $this->context->getViolations(); + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $node = new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ); + + $this->nodeTraverser->traverse(array($node), $this->context); + + return $this; } - /** - * Validates a value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value 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. - */ public function validateObject($object, $groups = null) { - $this->traverseObject($object, $groups); + $classMetadata = $this->metadataFactory->getMetadataFor($object); - return $this->context->getViolations(); + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $node = new ClassNode( + $object, + $classMetadata, + $this->defaultPropertyPath, + $groups + ); + + $this->nodeTraverser->traverse(array($node), $this->context); + + return $this; } public function validateCollection($collection, $groups = null, $deep = false) @@ -89,50 +117,85 @@ public function validateCollection($collection, $groups = null, $deep = false) 'deep' => $deep, )); - $this->traverse($collection, $constraint, $groups); - - return $this->context->getViolations(); + return $this->validate($collection, $constraint, $groups); } - /** - * Validates a property of a value against its current value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value containing the property. - * @param string $propertyName 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. - */ public function validateProperty($object, $propertyName, $groups = null) { - $this->traverseProperty($object, $propertyName, $groups); + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyValue = $propertyMetadata->getPropertyValue($object); + + $nodes[] = new PropertyNode( + $propertyValue, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups + ); + } + + $this->nodeTraverser->traverse($nodes, $this->context); - return $this->context->getViolations(); + return $this; } - /** - * Validate a property of a value against a potential value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param string $object The value containing the property. - * @param string $propertyName 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. - */ public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $this->traversePropertyValue($object, $propertyName, $value, $groups); + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $nodes[] = new PropertyNode( + $value, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, + $groups + ); + } + + $this->nodeTraverser->traverse($nodes, $this->context); + return $this; + } + + protected function normalizeGroups($groups) + { + if (is_array($groups)) { + return $groups; + } + + return array($groups); + } + + public function getViolations() + { return $this->context->getViolations(); } } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 61de8900f1431..69a224cb199b3 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -11,16 +11,80 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintViolationListInterface; + /** * @since %%NextVersion%% * @author Bernhard Schussek */ -interface ContextualValidatorInterface extends ValidatorInterface +interface ContextualValidatorInterface { /** - * @param $subPath + * @param string $subPath * - * @return ContextualValidatorInterface + * @return ContextualValidatorInterface This validator */ public function atPath($subPath); + + /** + * Validates a value against a constraint or a list of constraints. + * + * @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 ContextualValidatorInterface This validator + */ + public function validate($value, $constraints, $groups = null); + + /** + * Validates a value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value to validate + * @param array|null $groups The validation groups to validate. + * + * @return ContextualValidatorInterface This validator + */ + public function validateObject($object, $groups = null); + + public function validateCollection($collection, $groups = null, $deep = false); + + /** + * Validates a property of a value against its current value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value containing the property. + * @param string $propertyName The name of the property to validate. + * @param array|null $groups The validation groups to validate. + * + * @return ContextualValidatorInterface This validator + */ + public function validateProperty($object, $propertyName, $groups = null); + + /** + * Validate a property of a value against a potential value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param string $object The value containing the property. + * @param string $propertyName 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 ContextualValidatorInterface This validator + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null); + + /** + * @return ConstraintViolationListInterface + */ + public function getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 94857a3a1b555..820b01a149887 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Validator; -use Symfony\Component\Validator\Constraints\Traverse; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; @@ -20,67 +20,102 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class Validator extends AbstractValidator +class Validator implements ValidatorInterface { /** - * @var ExecutionContextManagerInterface + * @var ExecutionContextFactoryInterface */ - protected $contextManager; + protected $contextFactory; - public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextManagerInterface $contextManager) - { - parent::__construct($nodeTraverser, $metadataFactory); + /** + * @var NodeTraverserInterface + */ + protected $nodeTraverser; - $this->contextManager = $contextManager; + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) + { + $this->contextFactory = $contextFactory; + $this->nodeTraverser = $nodeTraverser; + $this->metadataFactory = $metadataFactory; } - public function validate($value, $constraints, $groups = null) + /** + * {@inheritdoc} + */ + public function startContext($root = null) { - $this->contextManager->startContext($value); + return new ContextualValidator( + $this->contextFactory->createContext($this, $root), + $this->nodeTraverser, + $this->metadataFactory + ); + } - $this->traverse($value, $constraints, $groups); + /** + * {@inheritdoc} + */ + public function inContext(ExecutionContextInterface $context) + { + return new ContextualValidator( + $context, + $this->nodeTraverser, + $this->metadataFactory + ); + } - return $this->contextManager->stopContext()->getViolations(); + /** + * {@inheritdoc} + */ + public function getMetadataFor($object) + { + return $this->metadataFactory->getMetadataFor($object); } - public function validateObject($object, $groups = null) + /** + * {@inheritdoc} + */ + public function hasMetadataFor($object) { - $this->contextManager->startContext($object); + return $this->metadataFactory->hasMetadataFor($object); + } - $this->traverseObject($object, $groups); + public function validate($value, $constraints, $groups = null) + { + return $this->startContext($value) + ->validate($value, $constraints, $groups) + ->getViolations(); + } - return $this->contextManager->stopContext()->getViolations(); + public function validateObject($object, $groups = null) + { + return $this->startContext($object) + ->validateObject($object, $groups) + ->getViolations(); } public function validateCollection($collection, $groups = null, $deep = false) { - $this->contextManager->startContext($collection); - - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); - - $this->traverse($collection, $constraint, $groups); - - return $this->contextManager->stopContext()->getViolations(); + return $this->startContext($collection) + ->validateCollection($collection, $groups, $deep) + ->getViolations(); } public function validateProperty($object, $propertyName, $groups = null) { - $this->contextManager->startContext($object); - - $this->traverseProperty($object, $propertyName, $groups); - - return $this->contextManager->stopContext()->getViolations(); + return $this->startContext($object) + ->validateProperty($object, $propertyName, $groups) + ->getViolations(); } public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $this->contextManager->startContext($object); - - $this->traversePropertyValue($object, $propertyName, $value, $groups); - - return $this->contextManager->stopContext()->getViolations(); + return $this->startContext($object) + ->validatePropertyValue($object, $propertyName, $value, $groups) + ->getViolations(); } } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 8cecaf3dc4b49..f69395284affd 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -81,6 +81,11 @@ public function validateProperty($object, $propertyName, $groups = null); */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); + /** + * @return ContextualValidatorInterface + */ + public function startContext(); + /** * @param ExecutionContextInterface $context * From e440690bb496c5d32bb892ef23714d6d9e407e3e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:20:50 +0100 Subject: [PATCH 041/323] [Validator] Renamed validateCollection() to validateObjects() --- .../Tests/Validator/Abstract2Dot5ApiTest.php | 6 +++--- .../Tests/Validator/AbstractLegacyApiTest.php | 4 ++-- .../Tests/Validator/AbstractValidatorTest.php | 12 ++++++------ .../Validator/Validator/ContextualValidator.php | 4 ++-- .../Validator/ContextualValidatorInterface.php | 2 +- .../Component/Validator/Validator/Validator.php | 6 +++--- .../Validator/Validator/ValidatorInterface.php | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index ad529f3750e92..97c982d116e2b 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -59,9 +59,9 @@ protected function validateObject($object, $groups = null) return $this->validator->validateObject($object, $groups); } - protected function validateCollection($collection, $groups = null, $deep = false) + protected function validateObjects($objects, $groups = null, $deep = false) { - return $this->validator->validateCollection($collection, $groups, $deep); + return $this->validator->validateObjects($objects, $groups, $deep); } protected function validateProperty($object, $propertyName, $groups = null) @@ -276,7 +276,7 @@ public function testValidateArrayInContext() ->getValidator() ->inContext($context) ->atPath('subpath') - ->validateCollection(array('key' => $value->reference)) + ->validateObjects(array('key' => $value->reference)) ; }; diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 4d76fa38c18aa..956537d22f90a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -57,9 +57,9 @@ protected function validateObject($object, $groups = null) return $this->validator->validate($object, $groups); } - protected function validateCollection($collection, $groups = null, $deep = false) + protected function validateObjects($objects, $groups = null, $deep = false) { - return $this->validator->validate($collection, $groups, true, $deep); + return $this->validator->validate($objects, $groups, true, $deep); } protected function validateProperty($object, $propertyName, $groups = null) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 99d250fb5a587..f4751e47c42a6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -67,7 +67,7 @@ abstract protected function validate($value, $constraints, $groups = null); abstract protected function validateObject($object, $groups = null); - abstract protected function validateCollection($collection, $groups = null, $deep = false); + abstract protected function validateObjects($objects, $groups = null, $deep = false); abstract protected function validateProperty($object, $propertyName, $groups = null); @@ -249,7 +249,7 @@ public function testArray() 'groups' => 'Group', ))); - $violations = $this->validateCollection($array, 'Group'); + $violations = $this->validateObjects($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -287,7 +287,7 @@ public function testRecursiveArray() 'groups' => 'Group', ))); - $violations = $this->validateCollection($array, 'Group'); + $violations = $this->validateObjects($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -325,7 +325,7 @@ public function testTraversable() 'groups' => 'Group', ))); - $violations = $this->validateCollection($traversable, 'Group'); + $violations = $this->validateObjects($traversable, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -359,7 +359,7 @@ public function testRecursiveTraversableRecursiveTraversalDisabled() 'groups' => 'Group', ))); - $this->validateCollection($traversable, 'Group'); + $this->validateObjects($traversable, 'Group'); } public function testRecursiveTraversableRecursiveTraversalEnabled() @@ -388,7 +388,7 @@ public function testRecursiveTraversableRecursiveTraversalEnabled() 'groups' => 'Group', ))); - $violations = $this->validateCollection($traversable, 'Group', true); + $violations = $this->validateObjects($traversable, 'Group', true); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index cb9951bf771da..588beaa42361e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -110,14 +110,14 @@ public function validateObject($object, $groups = null) return $this; } - public function validateCollection($collection, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null, $deep = false) { $constraint = new Traverse(array( 'traverse' => true, 'deep' => $deep, )); - return $this->validate($collection, $constraint, $groups); + return $this->validate($objects, $constraint, $groups); } public function validateProperty($object, $propertyName, $groups = null) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 69a224cb199b3..4e1a9a680b928 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -51,7 +51,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateCollection($collection, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null, $deep = false); /** * Validates a property of a value against its current value. diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 820b01a149887..fe7b80c817cda 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -98,10 +98,10 @@ public function validateObject($object, $groups = null) ->getViolations(); } - public function validateCollection($collection, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null, $deep = false) { - return $this->startContext($collection) - ->validateCollection($collection, $groups, $deep) + return $this->startContext($objects) + ->validateObjects($objects, $groups, $deep) ->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index f69395284affd..5eccf3f739491 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -47,7 +47,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateCollection($collection, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null, $deep = false); /** * Validates a property of a value against its current value. From e057b19964f831da36c5aaadc9b92b0855bc1441 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:35:17 +0100 Subject: [PATCH 042/323] [Validator] Decoupled ContextRefresher from ExecutionContext --- .../Validator/Context/ExecutionContext.php | 3 ++- .../NodeVisitor/ContextRefresher.php | 6 ++--- .../Validator/Util/NodeStackInterface.php | 25 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Validator/Util/NodeStackInterface.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 187de382fb84e..b4694492e636e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -20,6 +20,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Util\NodeStackInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -32,7 +33,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface +class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeStackInterface { /** * @var ValidatorInterface diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php index e647e7c1adfe1..56e8549942018 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Validator\NodeVisitor; -use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Util\NodeStackInterface; /** * Updates the current context with the current node of the validation @@ -27,7 +27,7 @@ class ContextRefresher extends AbstractVisitor { public function enterNode(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof ExecutionContext) { + if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. '"Symfony\Component\Validator\Context\ExecutionContext". '. @@ -46,7 +46,7 @@ public function enterNode(Node $node, ExecutionContextInterface $context) */ public function leaveNode(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof ExecutionContext) { + if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. '"Symfony\Component\Validator\Context\ExecutionContext". '. diff --git a/src/Symfony/Component/Validator/Util/NodeStackInterface.php b/src/Symfony/Component/Validator/Util/NodeStackInterface.php new file mode 100644 index 0000000000000..f330fa747e89f --- /dev/null +++ b/src/Symfony/Component/Validator/Util/NodeStackInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Util; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface NodeStackInterface +{ + public function pushNode(Node $node); + + public function popNode(); +} From 230f2a72fab5d0ad92eafc846bdb504827379020 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:36:09 +0100 Subject: [PATCH 043/323] [Validator] Fixed exception message --- .../Component/Validator/NodeVisitor/ContextRefresher.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php index 56e8549942018..3166d38285bc0 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php @@ -30,7 +30,7 @@ public function enterNode(Node $node, ExecutionContextInterface $context) if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. - '"Symfony\Component\Validator\Context\ExecutionContext". '. + '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) )); @@ -39,17 +39,12 @@ public function enterNode(Node $node, ExecutionContextInterface $context) $context->pushNode($node); } - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ public function leaveNode(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. - '"Symfony\Component\Validator\Context\ExecutionContext". '. + '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) )); From cf1281feefd59560c77bbb17cb980f20c4bb8161 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 11:36:21 +0100 Subject: [PATCH 044/323] [Validator] Added "Visitor" suffix to all node visitors --- .../Validator/NodeTraverser/NodeTraverser.php | 2 +- ...textRefresher.php => ContextRefresherVisitor.php} | 6 +++--- ...Resolver.php => GroupSequenceResolverVisitor.php} | 2 +- .../{NodeValidator.php => NodeValidatorVisitor.php} | 2 +- ...tInitializer.php => ObjectInitializerVisitor.php} | 2 +- .../Tests/Validator/LegacyValidator2Dot5ApiTest.php | 12 ++++++------ .../Tests/Validator/LegacyValidatorLegacyApiTest.php | 12 ++++++------ .../Tests/Validator/Validator2Dot5ApiTest.php | 12 ++++++------ 8 files changed, 25 insertions(+), 25 deletions(-) rename src/Symfony/Component/Validator/NodeVisitor/{ContextRefresher.php => ContextRefresherVisitor.php} (87%) rename src/Symfony/Component/Validator/NodeVisitor/{GroupSequenceResolver.php => GroupSequenceResolverVisitor.php} (96%) rename src/Symfony/Component/Validator/NodeVisitor/{NodeValidator.php => NodeValidatorVisitor.php} (98%) rename src/Symfony/Component/Validator/NodeVisitor/{ObjectInitializer.php => ObjectInitializerVisitor.php} (96%) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 9d6f4d639496f..fa4433564d801 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -137,7 +137,7 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) return; } - // The "cascadedGroups" property is set by the NodeValidator when + // The "cascadedGroups" property is set by the NodeValidatorVisitor when // traversing group sequences $cascadedGroups = null !== $node->cascadedGroups ? $node->cascadedGroups diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php similarity index 87% rename from src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php rename to src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php index 3166d38285bc0..3b940043a6848 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php @@ -23,13 +23,13 @@ * @since 2.5 * @author Bernhard Schussek */ -class ContextRefresher extends AbstractVisitor +class ContextRefresherVisitor extends AbstractVisitor { public function enterNode(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( - 'The ContextRefresher only supports instances of class '. + 'The ContextRefresherVisitor only supports instances of class '. '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) @@ -43,7 +43,7 @@ public function leaveNode(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( - 'The ContextRefresher only supports instances of class '. + 'The ContextRefresherVisitor only supports instances of class '. '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php rename to src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php index b5887ba4b0db0..c03f7a64c3801 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php @@ -21,7 +21,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class GroupSequenceResolver extends AbstractVisitor +class GroupSequenceResolverVisitor extends AbstractVisitor { public function enterNode(Node $node, ExecutionContextInterface $context) { diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php similarity index 98% rename from src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php index e410cb259e02e..6261791070d3f 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php @@ -24,7 +24,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class NodeValidator extends AbstractVisitor implements GroupManagerInterface +class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterface { private $validatedObjects = array(); diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php rename to src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php index 8d44d39ee0be7..1bef7f3189216 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php @@ -20,7 +20,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ObjectInitializer extends AbstractVisitor +class ObjectInitializerVisitor extends AbstractVisitor { /** * @var ObjectInitializerInterface[] diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 1cecafdedf9e0..600a297947f02 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresher; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolver(); - $contextRefresher = new ContextRefresher(); + $groupSequenceResolver = new GroupSequenceResolverVisitor(); + $contextRefresher = new ContextRefresherVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 94900e4497f52..fc2fe0bb38203 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresher; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolver(); - $contextRefresher = new ContextRefresher(); + $groupSequenceResolver = new GroupSequenceResolverVisitor(); + $contextRefresher = new ContextRefresherVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index 4d58f0a13e4c8..e3f92b879930a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresher; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\Validator; @@ -26,11 +26,11 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolver(); - $contextRefresher = new ContextRefresher(); + $groupSequenceResolver = new GroupSequenceResolverVisitor(); + $contextRefresher = new ContextRefresherVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); From 94583a92891560edcdf89fe5f42252c63fb54a86 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 16:28:28 +0100 Subject: [PATCH 045/323] [Validator] Changed NodeTraverser to traverse nodes iteratively, not recursively In this way, the deep method call chains are avoided. Also, it is possible to avoid the many calls to leaveNode(), which are currently not really needed. --- .../Validator/Context/ExecutionContext.php | 44 +------ .../Component/Validator/Node/PropertyNode.php | 18 ++- .../Validator/NodeTraverser/NodeTraverser.php | 116 ++++++------------ .../Validator/NodeTraverser/Traversal.php | 31 +++++ .../Validator/NodeVisitor/AbstractVisitor.php | 6 +- .../NodeVisitor/ContextRefresherVisitor.php | 23 +--- .../GroupSequenceResolverVisitor.php | 2 +- .../NodeObserverInterface.php} | 8 +- .../NodeVisitor/NodeValidatorVisitor.php | 18 +-- .../NodeVisitor/NodeVisitorInterface.php | 4 +- .../NodeVisitor/ObjectInitializerVisitor.php | 2 +- .../Tests/Context/ExecutionContextTest.php | 35 ------ .../Validator/ContextualValidator.php | 2 + 13 files changed, 107 insertions(+), 202 deletions(-) create mode 100644 src/Symfony/Component/Validator/NodeTraverser/Traversal.php rename src/Symfony/Component/Validator/{Util/NodeStackInterface.php => NodeVisitor/NodeObserverInterface.php} (71%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index b4694492e636e..b58d92350e440 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -20,7 +20,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Util\NodeStackInterface; +use Symfony\Component\Validator\NodeVisitor\NodeObserverInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -33,7 +33,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeStackInterface +class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeObserverInterface { /** * @var ValidatorInterface @@ -76,13 +76,6 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont */ private $node; - /** - * The trace of nodes from the root node to the current node. - * - * @var \SplStack - */ - private $nodeStack; - /** * Creates a new execution context. * @@ -108,49 +101,18 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn $this->translator = $translator; $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); - $this->nodeStack = new \SplStack(); } /** * Sets the values of the context to match the given node. * - * Internally, all nodes are stored on a stack and can be removed from that - * stack using {@link popNode()}. - * * @param Node $node The currently validated node */ - public function pushNode(Node $node) + public function setCurrentNode(Node $node) { - $this->nodeStack->push($node); $this->node = $node; } - /** - * Sets the values of the context to match the previous node. - * - * The current node is removed from the internal stack and returned. - * - * @return Node|null The currently validated node or null, if no node was - * on the stack - */ - public function popNode() - { - // Nothing to do if the stack is empty - if (0 === count($this->nodeStack)) { - return null; - } - - // Remove the current node from the stack - $poppedNode = $this->nodeStack->pop(); - - // Adjust the current node to the previous node - $this->node = count($this->nodeStack) > 0 - ? $this->nodeStack->top() - : null; - - return $poppedNode; - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 313da63ab7487..16136863a5b18 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; /** @@ -32,6 +33,11 @@ */ class PropertyNode extends Node { + /** + * @var object + */ + public $object; + /** * @var PropertyMetadataInterface */ @@ -40,6 +46,8 @@ class PropertyNode extends Node /** * Creates a new property node. * + * @param object $object The object the property + * belongs to * @param mixed $value The property value * @param PropertyMetadataInterface $metadata The property's metadata * @param string $propertyPath The property path leading @@ -49,9 +57,15 @@ class PropertyNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated + * + * @throws UnexpectedTypeException If $object is not an object */ - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { + if (!is_object($object)) { + throw new UnexpectedTypeException($object, 'object'); + } + parent::__construct( $value, $metadata, @@ -59,6 +73,8 @@ public function __construct($value, PropertyMetadataInterface $metadata, $proper $groups, $cascadedGroups ); + + $this->object = $object; } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index fa4433564d801..b2c4355b769b4 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -44,6 +44,7 @@ class NodeTraverser implements NodeTraverserInterface public function __construct(MetadataFactoryInterface $metadataFactory) { $this->visitors = new \SplObjectStorage(); + $this->nodeQueue = new \SplQueue(); $this->metadataFactory = $metadataFactory; } @@ -73,11 +74,19 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } + $traversal = new Traversal($context); + foreach ($nodes as $node) { - if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $context); - } else { - $this->traverseNode($node, $context); + $traversal->nodeQueue->enqueue($node); + + while (!$traversal->nodeQueue->isEmpty()) { + $node = $traversal->nodeQueue->dequeue(); + + if ($node instanceof ClassNode) { + $this->traverseClassNode($node, $traversal); + } else { + $this->traverseNode($node, $traversal); + } } } @@ -91,49 +100,31 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } - private function enterNode(Node $node, ExecutionContextInterface $context) + private function visit(Node $node, ExecutionContextInterface $context) { - $continueTraversal = true; - foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node, $context)) { - $continueTraversal = false; - - // Continue, so that the enterNode() method of all visitors - // is called + if (false === $visitor->visit($node, $context)) { + return false; } } - return $continueTraversal; + return true; } - private function leaveNode(Node $node, ExecutionContextInterface $context) + private function traverseNode(Node $node, Traversal $traversal) { - foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node, $context); + if (false === $this->visit($node, $traversal->context)) { + return; } - } - - private function traverseNode(Node $node, ExecutionContextInterface $context) - { - $continue = $this->enterNode($node, $context); // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $continue) { - $this->leaveNode($node, $context); - - return; - } - if (null === $node->value) { - $this->leaveNode($node, $context); - return; } @@ -144,8 +135,6 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) : $node->groups; if (0 === count($cascadedGroups)) { - $this->leaveNode($node, $context); - return; } @@ -161,11 +150,9 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $context + $traversal ); - $this->leaveNode($node, $context); - return; } @@ -178,25 +165,19 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $context + $traversal ); - $this->leaveNode($node, $context); - return; } // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node, $context); - return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node, $context); - return; } @@ -212,15 +193,15 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $context + $traversal ); - - $this->leaveNode($node, $context); } - private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) { - $continue = $this->enterNode($node, $context); + if (false === $this->visit($node, $traversal->context)) { + return; + } // Visitors have two possibilities to influence the traversal: // @@ -229,21 +210,14 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c // 2. If a visitor's enterNode() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $continue) { - $this->leaveNode($node, $context); - - return; - } - if (0 === count($node->groups)) { - $this->leaveNode($node, $context); - return; } foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $propertyNode = new PropertyNode( + $traversal->nodeQueue->enqueue(new PropertyNode( + $node->value, $propertyMetadata->getPropertyValue($node->value), $propertyMetadata, $node->propertyPath @@ -251,9 +225,7 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c : $propertyName, $node->groups, $node->cascadedGroups - ); - - $this->traverseNode($propertyNode, $context); + )); } } @@ -265,15 +237,11 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node, $context); - return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node, $context); - return; } @@ -289,13 +257,11 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c $node->propertyPath, $node->groups, $traversalStrategy, - $context + $traversal ); - - $this->leaveNode($node, $context); } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -304,14 +270,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal // error } - $classNode = new ClassNode( + $traversal->nodeQueue->enqueue(new ClassNode( $object, $classMetadata, $propertyPath, $groups - ); - - $this->traverseClassNode($classNode, $context, $traversalStrategy); + )); } catch (NoSuchMetadataException $e) { // Rethrow if the TRAVERSE bit is not set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { @@ -329,12 +293,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $propertyPath, $groups, $traversalStrategy, - $context + $traversal ); } } - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) { if ($traversalStrategy & TraversalStrategy::RECURSIVE) { // Try to traverse nested objects, but ignore if they do not @@ -357,7 +321,7 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $propertyPath.'['.$key.']', $groups, $traversalStrategy, - $context + $traversal ); continue; @@ -371,7 +335,7 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $propertyPath.'['.$key.']', $groups, $traversalStrategy, - $context + $traversal ); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/Traversal.php b/src/Symfony/Component/Validator/NodeTraverser/Traversal.php new file mode 100644 index 0000000000000..ab3a17e36c3b2 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/Traversal.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class Traversal +{ + public $context; + + public $nodeQueue; + + public function __construct(ExecutionContextInterface $context) + { + $this->context = $context; + $this->nodeQueue = new \SplQueue(); + } +} diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index 47631556f0d67..a47783ab0c8a7 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -28,11 +28,7 @@ public function afterTraversal(array $nodes, ExecutionContextInterface $context) { } - public function enterNode(Node $node, ExecutionContextInterface $context) - { - } - - public function leaveNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php index 3b940043a6848..b28b2f542fdad 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php @@ -14,7 +14,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Util\NodeStackInterface; /** * Updates the current context with the current node of the validation @@ -25,31 +24,17 @@ */ class ContextRefresherVisitor extends AbstractVisitor { - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof NodeStackInterface) { + if (!$context instanceof NodeObserverInterface) { throw new RuntimeException(sprintf( 'The ContextRefresherVisitor only supports instances of class '. - '"Symfony\Component\Validator\Context\NodeStackInterface". '. + '"Symfony\Component\Validator\NodeVisitor\NodeObserverInterface". '. 'An instance of class "%s" was given.', get_class($context) )); } - $context->pushNode($node); - } - - public function leaveNode(Node $node, ExecutionContextInterface $context) - { - if (!$context instanceof NodeStackInterface) { - throw new RuntimeException(sprintf( - 'The ContextRefresherVisitor only supports instances of class '. - '"Symfony\Component\Validator\Context\NodeStackInterface". '. - 'An instance of class "%s" was given.', - get_class($context) - )); - } - - $context->popNode(); + $context->setCurrentNode($node); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php index c03f7a64c3801..6b9330ec8d317 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php @@ -23,7 +23,7 @@ */ class GroupSequenceResolverVisitor extends AbstractVisitor { - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/Util/NodeStackInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php similarity index 71% rename from src/Symfony/Component/Validator/Util/NodeStackInterface.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php index f330fa747e89f..6588c09c90b90 100644 --- a/src/Symfony/Component/Validator/Util/NodeStackInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Util; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Node\Node; @@ -17,9 +17,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -interface NodeStackInterface +interface NodeObserverInterface { - public function pushNode(Node $node); - - public function popNode(); + public function setCurrentNode(Node $node); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php index 6261791070d3f..bded8c38a1798 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php @@ -42,29 +42,24 @@ class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterf private $currentGroup; - private $objectHashStack; - public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; - $this->objectHashStack = new \SplStack(); } public function afterTraversal(array $nodes, ExecutionContextInterface $context) { $this->validatedObjects = array(); $this->validatedConstraints = array(); - $this->objectHashStack = new \SplStack(); } - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); - $this->objectHashStack->push($objectHash); - } elseif ($node instanceof PropertyNode && count($this->objectHashStack) > 0) { - $objectHash = $this->objectHashStack->top(); + } elseif ($node instanceof PropertyNode) { + $objectHash = spl_object_hash($node->object); } else { $objectHash = null; } @@ -116,13 +111,6 @@ public function enterNode(Node $node, ExecutionContextInterface $context) return true; } - public function leaveNode(Node $node, ExecutionContextInterface $context) - { - if ($node instanceof ClassNode) { - $this->objectHashStack->pop(); - } - } - public function getCurrentGroup() { return $this->currentGroup; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index 6736cc23ba966..012a64191c292 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -24,7 +24,5 @@ public function beforeTraversal(array $nodes, ExecutionContextInterface $context public function afterTraversal(array $nodes, ExecutionContextInterface $context); - public function enterNode(Node $node, ExecutionContextInterface $context); - - public function leaveNode(Node $node, ExecutionContextInterface $context); + public function visit(Node $node, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php index 1bef7f3189216..01a2a07b5eb6b 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php @@ -43,7 +43,7 @@ public function __construct(array $initializers) $this->initializers = $initializers; } - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 8019aecea1ae4..45d3a70c12437 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -55,41 +55,6 @@ protected function setUp() ); } - public function testPushAndPop() - { - $metadata = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node = new GenericNode('value', $metadata, '', array(), array()); - - $this->context->pushNode($node); - - $this->assertSame('value', $this->context->getValue()); - // the other methods are covered in AbstractValidatorTest - - $this->assertSame($node, $this->context->popNode()); - - $this->assertNull($this->context->getValue()); - } - - public function testPushTwiceAndPop() - { - $metadata1 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node1 = new GenericNode('value', $metadata1, '', array(), array()); - $metadata2 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node2 = new GenericNode('other value', $metadata2, '', array(), array()); - - $this->context->pushNode($node1); - $this->context->pushNode($node2); - - $this->assertSame($node2, $this->context->popNode()); - - $this->assertSame('value', $this->context->getValue()); - } - - public function testPopWithoutPush() - { - $this->assertNull($this->context->popNode()); - } - public function testGetGroup() { $this->groupManager->expects($this->once()) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 588beaa42361e..2027240d29790 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -141,6 +141,7 @@ public function validateProperty($object, $propertyName, $groups = null) $propertyValue = $propertyMetadata->getPropertyValue($object); $nodes[] = new PropertyNode( + $object, $propertyValue, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), @@ -172,6 +173,7 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = foreach ($propertyMetadatas as $propertyMetadata) { $nodes[] = new PropertyNode( + $object, $value, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), From 117b1b9a17edba7b0724bc6ce52e3957e1229887 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 17:12:00 +0100 Subject: [PATCH 046/323] [Validator] Wrapped collections into CollectionNode instances --- .../Validator/Mapping/CollectionMetadata.php | 48 +++++++ .../Validator/Node/CollectionNode.php | 56 ++++++++ .../Validator/NodeTraverser/NodeTraverser.php | 124 +++++++++--------- 3 files changed, 169 insertions(+), 59 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/CollectionMetadata.php create mode 100644 src/Symfony/Component/Validator/Node/CollectionNode.php diff --git a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php new file mode 100644 index 0000000000000..f1235af899263 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class CollectionMetadata implements MetadataInterface +{ + private $traversalStrategy; + + public function __construct($traversalStrategy) + { + $this->traversalStrategy = $traversalStrategy; + } + + /** + * Returns all constraints for a given validation group. + * + * @param string $group The validation group. + * + * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. + */ + public function findConstraints($group) + { + return array(); + } + + public function getCascadingStrategy() + { + return CascadingStrategy::NONE; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } +} diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php new file mode 100644 index 0000000000000..848c7a6c929d7 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/CollectionNode.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\Node; + +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * Represents an traversable collection in the validation graph. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class CollectionNode extends Node +{ + /** + * Creates a new collection node. + * + * @param array|\Traversable $collection The validated collection + * @param MetadataInterface $metadata The class metadata of that + * object + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should be + * validated + * + * @throws UnexpectedTypeException If the given value is not an array or + * an instance of {@link \Traversable} + */ + public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + { + if (!is_array($collection) && !$collection instanceof \Traversable) { + throw new UnexpectedTypeException($collection, 'object'); + } + + parent::__construct( + $collection, + $metadata, + $propertyPath, + $groups, + $cascadedGroups + ); + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index b2c4355b769b4..7be878d713ff2 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -16,9 +16,11 @@ use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\CollectionMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; @@ -84,6 +86,8 @@ public function traverse(array $nodes, ExecutionContextInterface $context) if ($node instanceof ClassNode) { $this->traverseClassNode($node, $traversal); + } elseif ($node instanceof CollectionNode) { + $this->traverseCollectionNode($node, $traversal); } else { $this->traverseNode($node, $traversal); } @@ -145,13 +149,12 @@ private function traverseNode(Node $node, Traversal $traversal) // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $traversal - ); + $cascadedGroups + )); return; } @@ -188,13 +191,13 @@ private function traverseNode(Node $node, Traversal $traversal) )); } - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $traversal - ); + $node->groups, + $node->cascadedGroups + )); } private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) @@ -252,54 +255,23 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal, $trave )); } - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, $node->groups, - $traversalStrategy, - $traversal - ); + $node->cascadedGroups + )); } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) + private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - // error - } - - $traversal->nodeQueue->enqueue(new ClassNode( - $object, - $classMetadata, - $propertyPath, - $groups - )); - } catch (NoSuchMetadataException $e) { - // Rethrow if the TRAVERSE bit is not set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - throw $e; - } - - // Rethrow if the object does not implement Traversable - if (!$object instanceof \Traversable) { - throw $e; - } - - // In that case, iterate the object and cascade each entry - $this->cascadeEachObjectIn( - $object, - $propertyPath, - $groups, - $traversalStrategy, - $traversal - ); + if (false === $this->visit($node, $traversal->context)) { + return; } - } - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) - { + $traversalStrategy = $node->metadata->getTraversalStrategy(); + if ($traversalStrategy & TraversalStrategy::RECURSIVE) { // Try to traverse nested objects, but ignore if they do not // implement Traversable @@ -311,18 +283,17 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy = TraversalStrategy::IMPLICIT; } - foreach ($collection as $key => $value) { + foreach ($node->value as $key => $value) { if (is_array($value)) { // Arrays are always cascaded, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $traversal - ); + new CollectionMetadata($traversalStrategy), + $node->propertyPath.'['.$key.']', + $node->groups + )); continue; } @@ -332,12 +303,47 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, if (is_object($value)) { $this->cascadeObject( $value, - $propertyPath.'['.$key.']', - $groups, + $node->propertyPath.'['.$key.']', + $node->groups, $traversalStrategy, $traversal ); } } } + + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $traversal->nodeQueue->enqueue(new ClassNode( + $object, + $classMetadata, + $propertyPath, + $groups + )); + } catch (NoSuchMetadataException $e) { + // Rethrow if the TRAVERSE bit is not set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + throw $e; + } + + // Rethrow if the object does not implement Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + $traversal->nodeQueue->enqueue(new CollectionNode( + $object, + new CollectionMetadata($traversalStrategy), + $propertyPath, + $groups + )); + } + } } From 51197f68a398ae63c97e0527d43be30b77afdf12 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 19:10:26 +0100 Subject: [PATCH 047/323] [Validator] Made traversal of Traversables consistent If the traversal strategy is IMPLICIT (the default), the validator will now traverse any object that implements \Traversable and any array --- .../Validator/Constraints/Traverse.php | 2 +- .../Component/Validator/Constraints/Valid.php | 8 -- .../Validator/Mapping/ClassMetadata.php | 22 +++ .../Validator/Mapping/CollectionMetadata.php | 48 ------- .../Validator/Mapping/GenericMetadata.php | 22 +-- .../Validator/Mapping/MemberMetadata.php | 15 +-- .../Validator/Mapping/TraversalStrategy.php | 13 +- .../Component/Validator/Node/ClassNode.php | 8 +- .../Validator/Node/CollectionNode.php | 29 ++-- src/Symfony/Component/Validator/Node/Node.php | 28 ++-- .../Component/Validator/Node/PropertyNode.php | 7 +- .../Validator/NodeTraverser/NodeTraverser.php | 125 +++++++++--------- .../NodeVisitor/NodeValidatorVisitor.php | 5 + .../Tests/Validator/Abstract2Dot5ApiTest.php | 40 +++++- .../Tests/Validator/AbstractLegacyApiTest.php | 23 ++++ .../Tests/Validator/AbstractValidatorTest.php | 25 +--- .../Validator/ContextualValidator.php | 28 +++- .../ContextualValidatorInterface.php | 2 +- .../Validator/Validator/LegacyValidator.php | 16 +-- .../Validator/Validator/Validator.php | 4 +- .../Validator/ValidatorInterface.php | 2 +- 21 files changed, 244 insertions(+), 228 deletions(-) delete mode 100644 src/Symfony/Component/Validator/Mapping/CollectionMetadata.php diff --git a/src/Symfony/Component/Validator/Constraints/Traverse.php b/src/Symfony/Component/Validator/Constraints/Traverse.php index d9afe60c0745b..6c220561917f9 100644 --- a/src/Symfony/Component/Validator/Constraints/Traverse.php +++ b/src/Symfony/Component/Validator/Constraints/Traverse.php @@ -51,6 +51,6 @@ public function getDefaultOption() */ public function getTargets() { - return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); + return self::CLASS_CONSTRAINT; } } diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 9f15fdb04e1db..218f265f4f166 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -23,16 +23,8 @@ */ class Valid extends Constraint { - /** - * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. - * Use the {@link Traverse} constraint instead. - */ public $traverse = true; - /** - * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. - * Use the {@link Traverse} constraint instead. - */ public $deep = false; public function __construct($options = null) diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index d3c775b579cf1..5a2782f854041 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; @@ -190,6 +191,27 @@ public function addConstraint(Constraint $constraint) )); } + if ($constraint instanceof Traverse) { + if (true === $constraint->traverse) { + // If traverse is true, traversal should be explicitly enabled + $this->traversalStrategy = TraversalStrategy::TRAVERSE; + + if (!$constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; + } + } elseif (false === $constraint->traverse) { + // If traverse is false, traversal should be explicitly disabled + $this->traversalStrategy = TraversalStrategy::NONE; + } else { + // Else, traverse depending on the contextual information that + // is available during validation + $this->traversalStrategy = TraversalStrategy::IMPLICIT; + } + + // The constraint is not added + return $this; + } + $constraint->addImplicitGroupName($this->getDefaultGroup()); parent::addConstraint($constraint); diff --git a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php deleted file mode 100644 index f1235af899263..0000000000000 --- a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php +++ /dev/null @@ -1,48 +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\Mapping; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -class CollectionMetadata implements MetadataInterface -{ - private $traversalStrategy; - - public function __construct($traversalStrategy) - { - $this->traversalStrategy = $traversalStrategy; - } - - /** - * Returns all constraints for a given validation group. - * - * @param string $group The validation group. - * - * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. - */ - public function findConstraints($group) - { - return array(); - } - - public function getCascadingStrategy() - { - return CascadingStrategy::NONE; - } - - public function getTraversalStrategy() - { - return $this->traversalStrategy; - } -} diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 369276f220e89..69064a2cf494f 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -77,27 +77,17 @@ public function addConstraint(Constraint $constraint) if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; - return $this; - } - - if ($constraint instanceof Traverse) { - if (true === $constraint->traverse) { - // If traverse is true, traversal should be explicitly enabled - $this->traversalStrategy = TraversalStrategy::TRAVERSE; + if ($constraint->traverse) { + // Traverse unless the value is not traversable + $this->traversalStrategy = TraversalStrategy::IMPLICIT; - if ($constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + if (!$constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; } - } elseif (false === $constraint->traverse) { - // If traverse is false, traversal should be explicitly disabled - $this->traversalStrategy = TraversalStrategy::NONE; } else { - // Else, traverse depending on the contextual information that - // is available during validation - $this->traversalStrategy = TraversalStrategy::IMPLICIT; + $this->traversalStrategy = TraversalStrategy::NONE; } - // The constraint is not added return $this; } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 230af7d89e416..f23bdef04c7dd 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -60,17 +60,14 @@ public function addConstraint(Constraint $constraint) } // BC with Symfony < 2.5 - // Only process if the traversal strategy was not already set by the - // Traverse constraint - if ($constraint instanceof Valid && !$this->traversalStrategy) { + if ($constraint instanceof Valid) { if (true === $constraint->traverse) { // Try to traverse cascaded objects, but ignore if they do not // implement Traversable - $this->traversalStrategy = TraversalStrategy::TRAVERSE - | TraversalStrategy::IGNORE_NON_TRAVERSABLE; + $this->traversalStrategy = TraversalStrategy::IMPLICIT; - if ($constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + if (!$constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; } } elseif (false === $constraint->traverse) { $this->traversalStrategy = TraversalStrategy::NONE; @@ -180,7 +177,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return (boolean) ($this->traversalStrategy & TraversalStrategy::TRAVERSE); + return (boolean) ($this->traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE)); } /** @@ -191,7 +188,7 @@ public function isCollectionCascaded() */ public function isCollectionCascadedDeeply() { - return (boolean) ($this->traversalStrategy & TraversalStrategy::RECURSIVE); + return !($this->traversalStrategy & TraversalStrategy::STOP_RECURSION); } /** diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 951ec6058d87a..22b7f534550a2 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -17,15 +17,16 @@ */ class TraversalStrategy { - const IMPLICIT = 0; + /** + * @var integer + */ + const IMPLICIT = 1; - const NONE = 1; + const NONE = 2; - const TRAVERSE = 2; + const TRAVERSE = 4; - const RECURSIVE = 4; - - const IGNORE_NON_TRAVERSABLE = 8; + const STOP_RECURSION = 8; private function __construct() { diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 904e8651fdbc6..7c82eead9adec 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * Represents an object and its class metadata in the validation graph. @@ -40,10 +41,11 @@ class ClassNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated + * @param integer $traversalStrategy * - * @throws UnexpectedTypeException If the given value is not an object + * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -56,5 +58,7 @@ public function __construct($object, ClassMetadataInterface $metadata, $property $groups, $cascadedGroups ); + + $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index 848c7a6c929d7..2df442a4fc699 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * Represents an traversable collection in the validation graph. @@ -25,32 +27,35 @@ class CollectionNode extends Node /** * Creates a new collection node. * - * @param array|\Traversable $collection The validated collection - * @param MetadataInterface $metadata The class metadata of that - * object - * @param string $propertyPath The property path leading + * @param array|\Traversable $collection The validated collection + * @param string $propertyPath The property path leading * to this node - * @param string[] $groups The groups in which this + * @param string[] $groups The groups in which this * node should be validated - * @param string[]|null $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated + * @param integer $traversalStrategy The traversal strategy * - * @throws UnexpectedTypeException If the given value is not an array or - * an instance of {@link \Traversable} + * @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException */ - public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE) { if (!is_array($collection) && !$collection instanceof \Traversable) { - throw new UnexpectedTypeException($collection, 'object'); + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($collection) + )); } parent::__construct( $collection, - $metadata, + null, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); } } diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index b301db8dda696..038217bf512f9 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * A node in the validated graph. @@ -32,7 +33,7 @@ abstract class Node /** * The metadata specifying how the value should be validated. * - * @var MetadataInterface + * @var MetadataInterface|null */ public $metadata; @@ -57,21 +58,27 @@ abstract class Node */ public $cascadedGroups; + /** + * @var integer + */ + public $traversalStrategy; + /** * Creates a new property node. * - * @param mixed $value The property value - * @param MetadataInterface $metadata The property's metadata - * @param string $propertyPath The property path leading to - * this node - * @param string[] $groups The groups in which this node - * should be validated - * @param string[]|null $cascadedGroups The groups in which cascaded - * objects should be validated + * @param mixed $value The property value + * @param MetadataInterface|null $metadata The property's metadata + * @param string $propertyPath The property path leading to + * this node + * @param string[] $groups The groups in which this node + * should be validated + * @param string[]|null $cascadedGroups The groups in which cascaded + * objects should be validated + * @param integer $traversalStrategy * * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (null !== $cascadedGroups && !is_array($cascadedGroups)) { throw new UnexpectedTypeException($cascadedGroups, 'null or array'); @@ -82,5 +89,6 @@ public function __construct($value, MetadataInterface $metadata, $propertyPath, $this->propertyPath = $propertyPath; $this->groups = $groups; $this->cascadedGroups = $cascadedGroups; + $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 16136863a5b18..14dedb320356e 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * Represents the value of a property and its associated metadata. @@ -57,10 +58,11 @@ class PropertyNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated + * @param integer $traversalStrategy * * @throws UnexpectedTypeException If $object is not an object */ - public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -71,7 +73,8 @@ public function __construct($object, $value, PropertyMetadataInterface $metadata $metadata, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); $this->object = $object; diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 7be878d713ff2..44ab2eaa27b72 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -117,17 +117,17 @@ private function visit(Node $node, ExecutionContextInterface $context) private function traverseNode(Node $node, Traversal $traversal) { - if (false === $this->visit($node, $traversal->context)) { - return; - } - // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's visit() method returns false, the traversal is + // 1. If a visitor's enterNode() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, + // 2. If a visitor's enterNode() method removes a group from the node, // that group will be skipped in the subtree of that node. + if (false === $this->visit($node, $traversal->context)) { + return; + } + if (null === $node->value) { return; } @@ -151,9 +151,10 @@ private function traverseNode(Node $node, Traversal $traversal) // (BC with Symfony < 2.5) $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, - new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups + $cascadedGroups, + null, + $traversalStrategy )); return; @@ -174,38 +175,28 @@ private function traverseNode(Node $node, Traversal $traversal) return; } - // Traverse only if the TRAVERSE bit is set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { return; } - if (!$node->value instanceof \Traversable) { - if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - return; - } - - throw new ConstraintDefinitionException(sprintf( - 'Traversal was enabled for "%s", but this class '. - 'does not implement "\Traversable".', - get_class($node->value) - )); + // If IMPLICIT, stop unless we deal with a Traversable + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + return; } + // If TRAVERSE, the constructor will fail if we have no Traversable $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, - new CollectionMetadata($traversalStrategy), $node->propertyPath, - $node->groups, - $node->cascadedGroups + $cascadedGroups, + null, + $traversalStrategy )); } - private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseClassNode(ClassNode $node, Traversal $traversal) { - if (false === $this->visit($node, $traversal->context)) { - return; - } - // Visitors have two possibilities to influence the traversal: // // 1. If a visitor's enterNode() method returns false, the traversal is @@ -213,6 +204,10 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal, $trave // 2. If a visitor's enterNode() method removes a group from the node, // that group will be skipped in the subtree of that node. + if (false === $this->visit($node, $traversal->context)) { + return; + } + if (0 === count($node->groups)) { return; } @@ -232,54 +227,58 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal, $trave } } + $traversalStrategy = $node->traversalStrategy; + // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the class' metadata - if (TraversalStrategy::IMPLICIT === $traversalStrategy) { - $traversalStrategy = $node->metadata->getTraversalStrategy(); + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } - // Traverse only if the TRAVERSE bit is set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { return; } - if (!$node->value instanceof \Traversable) { - if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - return; - } - - throw new ConstraintDefinitionException(sprintf( - 'Traversal was enabled for "%s", but this class '. - 'does not implement "\Traversable".', - get_class($node->value) - )); + // If IMPLICIT, stop unless we deal with a Traversable + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + return; } + // If TRAVERSE, the constructor will fail if we have no Traversable $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, - new CollectionMetadata($traversalStrategy), $node->propertyPath, $node->groups, - $node->cascadedGroups + $node->cascadedGroups, + $traversalStrategy )); } private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) { + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + if (false === $this->visit($node, $traversal->context)) { return; } - $traversalStrategy = $node->metadata->getTraversalStrategy(); + if (0 === count($node->groups)) { + return; + } + + $traversalStrategy = $node->traversalStrategy; - if ($traversalStrategy & TraversalStrategy::RECURSIVE) { - // Try to traverse nested objects, but ignore if they do not - // implement Traversable - $traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE; + if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { + $traversalStrategy = TraversalStrategy::NONE; } else { - // If the RECURSIVE bit is not set, change the strategy to IMPLICIT - // in order to respect the metadata's traversal strategy of each entry - // in the collection $traversalStrategy = TraversalStrategy::IMPLICIT; } @@ -290,9 +289,10 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers // (BC with Symfony < 2.5) $traversal->nodeQueue->enqueue(new CollectionNode( $value, - new CollectionMetadata($traversalStrategy), $node->propertyPath.'['.$key.']', - $node->groups + $node->groups, + null, + $traversalStrategy )); continue; @@ -325,24 +325,27 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $object, $classMetadata, $propertyPath, - $groups + $groups, + null, + $traversalStrategy )); } catch (NoSuchMetadataException $e) { - // Rethrow if the TRAVERSE bit is not set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Rethrow if not Traversable + if (!$object instanceof \Traversable) { throw $e; } - // Rethrow if the object does not implement Traversable - if (!$object instanceof \Traversable) { + // Rethrow unless IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { throw $e; } $traversal->nodeQueue->enqueue(new CollectionNode( $object, - new CollectionMetadata($traversalStrategy), $propertyPath, - $groups + $groups, + null, + $traversalStrategy )); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php index bded8c38a1798..6cab8a3caced1 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; @@ -56,6 +57,10 @@ public function afterTraversal(array $nodes, ExecutionContextInterface $context) public function visit(Node $node, ExecutionContextInterface $context) { + if ($node instanceof CollectionNode) { + return true; + } + if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 97c982d116e2b..04f42480cedcb 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\Reference; @@ -354,14 +355,43 @@ public function testValidateAcceptsValid() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ - public function testExpectTraversableIfTraverse() + public function testTraverseTraversableByDefault() { + $test = $this; $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; - $this->validator->validate($entity, new Traverse()); + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($traversable, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); } /** diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 956537d22f90a..d3b5f1ba6cb20 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -93,6 +93,29 @@ public function testTraversableTraverseDisabled() $this->validator->validate($traversable, 'Group'); } + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + public function testValidateInContext() { $test = $this; diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index f4751e47c42a6..c413f0a44afb4 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -339,30 +339,7 @@ public function testTraversable() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testRecursiveTraversableRecursiveTraversalDisabled() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => $entity)), - )); - - $callback = function () use ($test) { - $test->fail('Should not be called'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $this->validateObjects($traversable, 'Group'); - } - - public function testRecursiveTraversableRecursiveTraversalEnabled() + public function testRecursiveTraversable() { $test = $this; $entity = new Entity(); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 2027240d29790..74850d8ce638b 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -14,11 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; +use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; @@ -110,14 +114,26 @@ public function validateObject($object, $groups = null) return $this; } - public function validateObjects($objects, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null) { - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); + if (!is_array($objects) && !$objects instanceof \Traversable) { + throw new UnexpectedTypeException($objects, 'array or \Traversable'); + } + + $traversalStrategy = TraversalStrategy::TRAVERSE; + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - return $this->validate($objects, $constraint, $groups); + $node = new CollectionNode( + $objects, + $this->defaultPropertyPath, + $groups, + null, + $traversalStrategy + ); + + $this->nodeTraverser->traverse(array($node), $this->context); + + return $this; } public function validateProperty($object, $propertyName, $groups = null) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 4e1a9a680b928..23aece1f53101 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -51,7 +51,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateObjects($objects, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null); /** * Validates a property of a value against its current value. diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 73d6948e3ece4..eb17013675882 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -29,24 +29,12 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals return parent::validate($value, $groups, $traverse); } - if (is_array($value)) { - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); + if (is_array($value) || ($traverse && $value instanceof \Traversable)) { + $constraint = new Valid(array('deep' => $deep)); return parent::validate($value, $constraint, $groups); } - if ($traverse && $value instanceof \Traversable) { - $constraints = array( - new Valid(), - new Traverse(array('traverse' => true, 'deep' => $deep)), - ); - - return parent::validate($value, $constraints, $groups); - } - return $this->validateObject($value, $groups); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index fe7b80c817cda..1484dae715ac4 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -98,10 +98,10 @@ public function validateObject($object, $groups = null) ->getViolations(); } - public function validateObjects($objects, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null) { return $this->startContext($objects) - ->validateObjects($objects, $groups, $deep) + ->validateObjects($objects, $groups) ->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 5eccf3f739491..95a1342af4fb4 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -47,7 +47,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateObjects($objects, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null); /** * Validates a property of a value against its current value. From 08172bfe7bd4ef4b22e129c953552179ed7b4c84 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 19:57:14 +0100 Subject: [PATCH 048/323] [Validator] Merged validate(), validateObject() and validateObjects() to simplify usage --- .../Component/Validator/Constraints/Valid.php | 2 +- .../Context/LegacyExecutionContext.php | 2 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 74 +++------------ .../Tests/Validator/AbstractLegacyApiTest.php | 21 ++--- .../Tests/Validator/AbstractValidatorTest.php | 91 +++++++------------ .../Validator/ContextualValidator.php | 56 +----------- .../ContextualValidatorInterface.php | 20 +--- .../Validator/Validator/LegacyValidator.php | 29 ++++-- .../Validator/Validator/Validator.php | 16 +--- .../Validator/ValidatorInterface.php | 21 +---- 10 files changed, 93 insertions(+), 239 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 218f265f4f166..6ad09624b57d4 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -25,7 +25,7 @@ class Valid extends Constraint { public $traverse = true; - public $deep = false; + public $deep = true; public function __construct($options = null) { diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 2d85ab7905a19..7e3b5971aacdb 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -143,7 +143,7 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateObject($value, $groups) + ->validate($value, null, $groups) ; } diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 04f42480cedcb..973283c84e88c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -50,21 +50,11 @@ protected function setUp() $this->validator = $this->createValidator($this->metadataFactory); } - protected function validate($value, $constraints, $groups = null) + protected function validate($value, $constraints = null, $groups = null) { return $this->validator->validate($value, $constraints, $groups); } - protected function validateObject($object, $groups = null) - { - return $this->validator->validateObject($object, $groups); - } - - protected function validateObjects($objects, $groups = null, $deep = false) - { - return $this->validator->validateObjects($objects, $groups, $deep); - } - protected function validateProperty($object, $propertyName, $groups = null) { return $this->validator->validateProperty($object, $propertyName, $groups); @@ -88,7 +78,7 @@ public function testNoDuplicateValidationIfConstraintInMultipleGroups() 'groups' => array('Group 1', 'Group 2'), ))); - $violations = $this->validator->validateObject($entity, array('Group 1', 'Group 2')); + $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -119,7 +109,7 @@ public function testGroupSequenceAbortsAfterFailedGroup() ))); $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); - $violations = $this->validator->validateObject($entity, $sequence); + $violations = $this->validator->validate($entity, new Valid(), $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -149,7 +139,7 @@ public function testGroupSequenceIncludesReferences() ))); $sequence = new GroupSequence(array('Group 1', 'Entity')); - $violations = $this->validator->validateObject($entity, $sequence); + $violations = $this->validator->validate($entity, new Valid(), $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -167,7 +157,7 @@ public function testValidateInSeparateContext() ->getValidator() // Since the validator is not context aware, the group must // be passed explicitly - ->validateObject($value->reference, 'Group') + ->validate($value->reference, new Valid(), 'Group') ; /** @var ConstraintViolationInterface[] $violations */ @@ -208,7 +198,7 @@ public function testValidateInSeparateContext() 'groups' => 'Group', ))); - $violations = $this->validator->validateObject($entity, 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -226,7 +216,7 @@ public function testValidateInContext() ->getValidator() ->inContext($context) ->atPath('subpath') - ->validateObject($value->reference) + ->validate($value->reference) ; }; @@ -252,7 +242,7 @@ public function testValidateInContext() 'groups' => 'Group', ))); - $violations = $this->validator->validateObject($entity, 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -277,7 +267,7 @@ public function testValidateArrayInContext() ->getValidator() ->inContext($context) ->atPath('subpath') - ->validateObjects(array('key' => $value->reference)) + ->validate(array('key' => $value->reference)) ; }; @@ -303,7 +293,7 @@ public function testValidateArrayInContext() 'groups' => 'Group', ))); - $violations = $this->validator->validateObject($entity, 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -317,44 +307,6 @@ public function testValidateArrayInContext() $this->assertNull($violations[0]->getCode()); } - public function testValidateAcceptsValid() - { - $test = $this; - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - // This is the same as when calling validateObject() - $violations = $this->validator->validate($entity, new Valid(), 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - public function testTraverseTraversableByDefault() { $test = $this; @@ -380,7 +332,7 @@ public function testTraverseTraversableByDefault() 'groups' => 'Group', ))); - $violations = $this->validateObject($traversable, 'Group'); + $violations = $this->validate($traversable, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -403,7 +355,7 @@ public function testExpectTraversableIfTraverseOnClass() $this->metadata->addConstraint(new Traverse()); - $this->validator->validateObject($entity); + $this->validator->validate($entity); } public function testAddCustomizedViolation() @@ -421,7 +373,7 @@ public function testAddCustomizedViolation() $this->metadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validateObject($entity); + $violations = $this->validator->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index d3b5f1ba6cb20..7bca5de9b6866 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; @@ -47,19 +48,17 @@ protected function setUp() $this->validator = $this->createValidator($this->metadataFactory); } - protected function validate($value, $constraints, $groups = null) + protected function validate($value, $constraints = null, $groups = null) { - return $this->validator->validateValue($value, $constraints, $groups); - } + if (null === $constraints) { + $constraints = new Valid(); + } - protected function validateObject($object, $groups = null) - { - return $this->validator->validate($object, $groups); - } + if ($constraints instanceof Valid) { + return $this->validator->validate($value, $groups, $constraints->traverse, $constraints->deep); + } - protected function validateObjects($objects, $groups = null, $deep = false) - { - return $this->validator->validate($objects, $groups, true, $deep); + return $this->validator->validateValue($value, $constraints, $groups); } protected function validateProperty($object, $propertyName, $groups = null) @@ -226,7 +225,7 @@ public function testAddCustomizedViolation() $this->metadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validator->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index c413f0a44afb4..4c830d25d4bff 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -63,11 +63,7 @@ protected function tearDown() $this->referenceMetadata = null; } - abstract protected function validate($value, $constraints, $groups = null); - - abstract protected function validateObject($object, $groups = null); - - abstract protected function validateObjects($objects, $groups = null, $deep = false); + abstract protected function validate($value, $constraints = null, $groups = null); abstract protected function validateProperty($object, $propertyName, $groups = null); @@ -131,7 +127,7 @@ public function testClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -171,7 +167,7 @@ public function testPropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -211,7 +207,7 @@ public function testGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -249,7 +245,7 @@ public function testArray() 'groups' => 'Group', ))); - $violations = $this->validateObjects($array, 'Group'); + $violations = $this->validate($array, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -287,7 +283,7 @@ public function testRecursiveArray() 'groups' => 'Group', ))); - $violations = $this->validateObjects($array, 'Group'); + $violations = $this->validate($array, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -325,7 +321,7 @@ public function testTraversable() 'groups' => 'Group', ))); - $violations = $this->validateObjects($traversable, 'Group'); + $violations = $this->validate($traversable, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -365,7 +361,7 @@ public function testRecursiveTraversable() 'groups' => 'Group', ))); - $violations = $this->validateObjects($traversable, 'Group', true); + $violations = $this->validate($traversable, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -404,7 +400,7 @@ public function testReferenceClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -446,7 +442,7 @@ public function testReferencePropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -488,7 +484,7 @@ public function testReferenceGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -509,7 +505,7 @@ public function testsIgnoreNullReference() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -525,7 +521,7 @@ public function testFailOnScalarReferences() $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->validateObject($entity); + $this->validate($entity); } public function testArrayReference() @@ -553,7 +549,7 @@ public function testArrayReference() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -593,7 +589,7 @@ public function testRecursiveArrayReference() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -621,7 +617,7 @@ public function testArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -641,7 +637,7 @@ public function testRecursiveArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -654,7 +650,7 @@ public function testIgnoreScalarsDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -667,7 +663,7 @@ public function testIgnoreNullDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -698,7 +694,7 @@ public function testTraversableReference() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -727,7 +723,7 @@ public function testDisableTraversableTraversal() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -745,28 +741,7 @@ public function testMetadataMustExistIfTraversalIsDisabled() 'traverse' => false, ))); - $this->validateObject($entity, 'Default', ''); - } - - public function testNoRecursiveTraversableTraversal() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => new Reference())), - )); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback($callback)); - - $violations = $this->validateObject($entity); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(0, $violations); + $this->validate($entity); } public function testEnableRecursiveTraversableTraversal() @@ -798,7 +773,7 @@ public function testEnableRecursiveTraversableTraversal() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -954,7 +929,7 @@ public function testValidateObjectOnlyOncePerGroup() $this->metadata->addPropertyConstraint('reference2', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -974,7 +949,7 @@ public function testValidateDifferentObjectsSeparately() $this->metadata->addPropertyConstraint('reference2', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); @@ -997,7 +972,7 @@ public function testValidateSingleGroup() 'groups' => 'Group 2', ))); - $violations = $this->validateObject($entity, 'Group 2'); + $violations = $this->validate($entity, null, 'Group 2'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1020,7 +995,7 @@ public function testValidateMultipleGroups() 'groups' => 'Group 2', ))); - $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); + $violations = $this->validate($entity, null, array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); @@ -1053,7 +1028,7 @@ public function testReplaceDefaultGroupByGroupSequenceObject() $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1087,7 +1062,7 @@ public function testReplaceDefaultGroupByGroupSequenceArray() $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1119,7 +1094,7 @@ public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1149,7 +1124,7 @@ public function testValidateCustomGroupWhenDefaultGroupWasReplaced() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Other Group'); + $violations = $this->validate($entity, null, 'Other Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1185,7 +1160,7 @@ public function testReplaceDefaultGroupWithObjectFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1221,7 +1196,7 @@ public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 74850d8ce638b..e9b208eae316e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -65,9 +66,11 @@ public function atPath($subPath) return $this; } - public function validate($value, $constraints, $groups = null) + public function validate($value, $constraints = null, $groups = null) { - if (!is_array($constraints)) { + if (null === $constraints) { + $constraints = array(new Valid()); + } elseif (!is_array($constraints)) { $constraints = array($constraints); } @@ -87,55 +90,6 @@ public function validate($value, $constraints, $groups = null) return $this; } - public function validateObject($object, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $node = new ClassNode( - $object, - $classMetadata, - $this->defaultPropertyPath, - $groups - ); - - $this->nodeTraverser->traverse(array($node), $this->context); - - return $this; - } - - public function validateObjects($objects, $groups = null) - { - if (!is_array($objects) && !$objects instanceof \Traversable) { - throw new UnexpectedTypeException($objects, 'array or \Traversable'); - } - - $traversalStrategy = TraversalStrategy::TRAVERSE; - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $node = new CollectionNode( - $objects, - $this->defaultPropertyPath, - $groups, - null, - $traversalStrategy - ); - - $this->nodeTraverser->traverse(array($node), $this->context); - - return $this; - } - public function validateProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 23aece1f53101..e384228d7c977 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -30,28 +30,16 @@ public function atPath($subPath); /** * Validates a value against a constraint or a list of constraints. * + * If no constraint is passed, the constraint + * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. + * * @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 ContextualValidatorInterface This validator */ - public function validate($value, $constraints, $groups = null); - - /** - * Validates a value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value to validate - * @param array|null $groups The validation groups to validate. - * - * @return ContextualValidatorInterface This validator - */ - public function validateObject($object, $groups = null); - - public function validateObjects($objects, $groups = null); + public function validate($value, $constraints = null, $groups = null); /** * Validates a property of a value against its current value. diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index eb17013675882..acebb5824da70 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -24,18 +25,20 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { - // Use new signature if constraints are given in the second argument - if (func_num_args() <= 3 && ($groups instanceof Constraint || (is_array($groups) && current($groups) instanceof Constraint))) { - return parent::validate($value, $groups, $traverse); - } + $numArgs = func_num_args(); - if (is_array($value) || ($traverse && $value instanceof \Traversable)) { - $constraint = new Valid(array('deep' => $deep)); + // Use new signature if constraints are given in the second argument + if (self::testConstraints($groups) && ($numArgs < 2 || 3 === $numArgs && self::testGroups($traverse))) { + // Rename to avoid total confusion ;) + $constraints = $groups; + $groups = $traverse; - return parent::validate($value, $constraint, $groups); + return parent::validate($value, $constraints, $groups); } - return $this->validateObject($value, $groups); + $constraint = new Valid(array('traverse' => $traverse, 'deep' => $deep)); + + return parent::validate($value, $constraint, $groups); } public function validateValue($value, $constraints, $groups = null) @@ -47,4 +50,14 @@ public function getMetadataFactory() { return $this->metadataFactory; } + + private static function testConstraints($constraints) + { + return null === $constraints || $constraints instanceof Constraint || (is_array($constraints) && current($constraints) instanceof Constraint); + } + + private static function testGroups($groups) + { + return null === $groups || is_string($groups) || $groups instanceof GroupSequence || (is_array($groups) && (is_string(current($groups)) || current($groups) instanceof GroupSequence)); + } } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 1484dae715ac4..fad080155af5f 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -84,27 +84,13 @@ public function hasMetadataFor($object) return $this->metadataFactory->hasMetadataFor($object); } - public function validate($value, $constraints, $groups = null) + public function validate($value, $constraints = null, $groups = null) { return $this->startContext($value) ->validate($value, $constraints, $groups) ->getViolations(); } - public function validateObject($object, $groups = null) - { - return $this->startContext($object) - ->validateObject($object, $groups) - ->getViolations(); - } - - public function validateObjects($objects, $groups = null) - { - return $this->startContext($objects) - ->validateObjects($objects, $groups) - ->getViolations(); - } - public function validateProperty($object, $propertyName, $groups = null) { return $this->startContext($object) diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 95a1342af4fb4..124514a1ffaf7 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -24,6 +24,9 @@ interface ValidatorInterface /** * Validates a value against a constraint or a list of constraints. * + * If no constraint is passed, the constraint + * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. + * * @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. @@ -31,23 +34,7 @@ interface ValidatorInterface * @return ConstraintViolationListInterface A list of constraint violations. If the * list is empty, validation succeeded. */ - public function validate($value, $constraints, $groups = null); - - /** - * Validates a value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value 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. - */ - public function validateObject($object, $groups = null); - - public function validateObjects($objects, $groups = null); + public function validate($value, $constraints = null, $groups = null); /** * Validates a property of a value against its current value. From aeb68228b1906ea82d77e0dfb6f333ff9285fe6c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:05:44 +0100 Subject: [PATCH 049/323] [Validator] Improved visitor names --- .../Validator/NodeTraverser/NodeTraverser.php | 4 +--- ...RefresherVisitor.php => ContextUpdateVisitor.php} | 4 ++-- ...Visitor.php => GroupSequenceResolvingVisitor.php} | 2 +- ...alidatorVisitor.php => NodeValidationVisitor.php} | 2 +- ...erVisitor.php => ObjectInitializationVisitor.php} | 2 +- .../Tests/Validator/LegacyValidator2Dot5ApiTest.php | 12 ++++++------ .../Tests/Validator/LegacyValidatorLegacyApiTest.php | 12 ++++++------ .../Tests/Validator/Validator2Dot5ApiTest.php | 12 ++++++------ 8 files changed, 24 insertions(+), 26 deletions(-) rename src/Symfony/Component/Validator/NodeVisitor/{ContextRefresherVisitor.php => ContextUpdateVisitor.php} (88%) rename src/Symfony/Component/Validator/NodeVisitor/{GroupSequenceResolverVisitor.php => GroupSequenceResolvingVisitor.php} (96%) rename src/Symfony/Component/Validator/NodeVisitor/{NodeValidatorVisitor.php => NodeValidationVisitor.php} (98%) rename src/Symfony/Component/Validator/NodeVisitor/{ObjectInitializerVisitor.php => ObjectInitializationVisitor.php} (96%) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 44ab2eaa27b72..1c1e5261e8cf9 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,11 +12,9 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\CollectionMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -132,7 +130,7 @@ private function traverseNode(Node $node, Traversal $traversal) return; } - // The "cascadedGroups" property is set by the NodeValidatorVisitor when + // The "cascadedGroups" property is set by the NodeValidationVisitor when // traversing group sequences $cascadedGroups = null !== $node->cascadedGroups ? $node->cascadedGroups diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php similarity index 88% rename from src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php index b28b2f542fdad..4fb7f1b33a428 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php @@ -22,13 +22,13 @@ * @since 2.5 * @author Bernhard Schussek */ -class ContextRefresherVisitor extends AbstractVisitor +class ContextUpdateVisitor extends AbstractVisitor { public function visit(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeObserverInterface) { throw new RuntimeException(sprintf( - 'The ContextRefresherVisitor only supports instances of class '. + 'The ContextUpdateVisitor only supports instances of class '. '"Symfony\Component\Validator\NodeVisitor\NodeObserverInterface". '. 'An instance of class "%s" was given.', get_class($context) diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php index 6b9330ec8d317..2d9b68737a1d0 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php @@ -21,7 +21,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class GroupSequenceResolverVisitor extends AbstractVisitor +class GroupSequenceResolvingVisitor extends AbstractVisitor { public function visit(Node $node, ExecutionContextInterface $context) { diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php similarity index 98% rename from src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 6cab8a3caced1..c688295da6ae4 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -25,7 +25,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterface +class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface { private $validatedObjects = array(); diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php index 01a2a07b5eb6b..05226ac6d359b 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php @@ -20,7 +20,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ObjectInitializerVisitor extends AbstractVisitor +class ObjectInitializationVisitor extends AbstractVisitor { /** * @var ObjectInitializerInterface[] diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 600a297947f02..e61daf9f939cd 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolverVisitor(); - $contextRefresher = new ContextRefresherVisitor(); + $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index fc2fe0bb38203..672f2ebf42c9a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolverVisitor(); - $contextRefresher = new ContextRefresherVisitor(); + $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index e3f92b879930a..b49c380c98708 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\Validator; @@ -26,11 +26,11 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolverVisitor(); - $contextRefresher = new ContextRefresherVisitor(); + $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); From 416137165eaa7d48425ead91158b726899857a47 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:12:57 +0100 Subject: [PATCH 050/323] [Validator] Removed unused use statements --- src/Symfony/Component/Validator/Mapping/GenericMetadata.php | 1 - src/Symfony/Component/Validator/Node/CollectionNode.php | 2 -- .../Validator/Tests/Validator/AbstractLegacyApiTest.php | 1 - .../Component/Validator/Validator/ContextualValidator.php | 6 ------ .../Component/Validator/Validator/LegacyValidator.php | 1 - 5 files changed, 11 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 69064a2cf494f..a87903dec0258 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; /** diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index 2df442a4fc699..763ed63072c00 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Validator\Node; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; /** diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 7bca5de9b6866..386c250a285e6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index e9b208eae316e..cd969cc5b152e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -12,18 +12,12 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index acebb5824da70..5bc570737625e 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; From 76d8c9a1f718c902adb7f886107db42e9a1ffc97 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:15:27 +0100 Subject: [PATCH 051/323] [Validator] Fixed typos --- .../Validator/NodeTraverser/NodeTraverser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 1c1e5261e8cf9..574179648061a 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -117,9 +117,9 @@ private function traverseNode(Node $node, Traversal $traversal) { // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. if (false === $this->visit($node, $traversal->context)) { @@ -197,9 +197,9 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) { // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. if (false === $this->visit($node, $traversal->context)) { @@ -259,9 +259,9 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers { // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. if (false === $this->visit($node, $traversal->context)) { From 778ec2458b802b5a5569b5db13c0cdc2220870b3 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:23:55 +0100 Subject: [PATCH 052/323] [Validator] Removed helper class Traversal --- .../Validator/NodeTraverser/NodeTraverser.php | 46 +++++++++---------- .../Validator/NodeTraverser/Traversal.php | 31 ------------- 2 files changed, 23 insertions(+), 54 deletions(-) delete mode 100644 src/Symfony/Component/Validator/NodeTraverser/Traversal.php diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 574179648061a..be9f2d2b880e8 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -74,20 +74,20 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } - $traversal = new Traversal($context); + $nodeQueue = new \SplQueue(); foreach ($nodes as $node) { - $traversal->nodeQueue->enqueue($node); + $nodeQueue->enqueue($node); - while (!$traversal->nodeQueue->isEmpty()) { - $node = $traversal->nodeQueue->dequeue(); + while (!$nodeQueue->isEmpty()) { + $node = $nodeQueue->dequeue(); if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $traversal); + $this->traverseClassNode($node, $nodeQueue, $context); } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $traversal); + $this->traverseCollectionNode($node, $nodeQueue, $context); } else { - $this->traverseNode($node, $traversal); + $this->traverseNode($node, $nodeQueue, $context); } } } @@ -113,7 +113,7 @@ private function visit(Node $node, ExecutionContextInterface $context) return true; } - private function traverseNode(Node $node, Traversal $traversal) + private function traverseNode(Node $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) { // Visitors have two possibilities to influence the traversal: // @@ -122,7 +122,7 @@ private function traverseNode(Node $node, Traversal $traversal) // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $this->visit($node, $traversal->context)) { + if (false === $this->visit($node, $context)) { return; } @@ -147,7 +147,7 @@ private function traverseNode(Node $node, Traversal $traversal) // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $node->value, $node->propertyPath, $cascadedGroups, @@ -167,7 +167,7 @@ private function traverseNode(Node $node, Traversal $traversal) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $traversal + $nodeQueue ); return; @@ -184,7 +184,7 @@ private function traverseNode(Node $node, Traversal $traversal) } // If TRAVERSE, the constructor will fail if we have no Traversable - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $node->value, $node->propertyPath, $cascadedGroups, @@ -193,7 +193,7 @@ private function traverseNode(Node $node, Traversal $traversal) )); } - private function traverseClassNode(ClassNode $node, Traversal $traversal) + private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) { // Visitors have two possibilities to influence the traversal: // @@ -202,7 +202,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $this->visit($node, $traversal->context)) { + if (false === $this->visit($node, $context)) { return; } @@ -212,7 +212,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $traversal->nodeQueue->enqueue(new PropertyNode( + $nodeQueue->enqueue(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), $propertyMetadata, @@ -246,7 +246,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) } // If TRAVERSE, the constructor will fail if we have no Traversable - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $node->value, $node->propertyPath, $node->groups, @@ -255,7 +255,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) )); } - private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) + private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) { // Visitors have two possibilities to influence the traversal: // @@ -264,7 +264,7 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $this->visit($node, $traversal->context)) { + if (false === $this->visit($node, $context)) { return; } @@ -285,7 +285,7 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers // Arrays are always cascaded, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $value, $node->propertyPath.'['.$key.']', $node->groups, @@ -304,13 +304,13 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers $node->propertyPath.'['.$key.']', $node->groups, $traversalStrategy, - $traversal + $nodeQueue ); } } } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplQueue $nodeQueue) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -319,7 +319,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal // error } - $traversal->nodeQueue->enqueue(new ClassNode( + $nodeQueue->enqueue(new ClassNode( $object, $classMetadata, $propertyPath, @@ -338,7 +338,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal throw $e; } - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $object, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/NodeTraverser/Traversal.php b/src/Symfony/Component/Validator/NodeTraverser/Traversal.php deleted file mode 100644 index ab3a17e36c3b2..0000000000000 --- a/src/Symfony/Component/Validator/NodeTraverser/Traversal.php +++ /dev/null @@ -1,31 +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\NodeTraverser; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -class Traversal -{ - public $context; - - public $nodeQueue; - - public function __construct(ExecutionContextInterface $context) - { - $this->context = $context; - $this->nodeQueue = new \SplQueue(); - } -} From 6fc6ecdd8e89b0350e5fb99fafb9f8a5a8f07431 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:47:32 +0100 Subject: [PATCH 053/323] [Validator] Fixed tests under PHP<5.3.9 The new implementation is disabled on those PHP versions. --- .../Mapping/ClassMetadataInterface.php | 18 ++---------------- .../Validator/Mapping/GenericMetadata.php | 9 +++++++++ .../Validator/Mapping/MemberMetadata.php | 3 +-- .../Validator/Mapping/MetadataInterface.php | 13 +++---------- .../Mapping/PropertyMetadataInterface.php | 18 ++---------------- .../Tests/Constraints/CountryValidatorTest.php | 2 +- .../Constraints/CurrencyValidatorTest.php | 2 +- .../Constraints/LanguageValidatorTest.php | 2 +- .../Tests/Context/ExecutionContextTest.php | 4 ++++ .../Tests/Validator/AbstractValidatorTest.php | 6 +++--- .../Validator/LegacyValidator2Dot5ApiTest.php | 9 +++++++++ .../Validator/LegacyValidatorLegacyApiTest.php | 9 +++++++++ .../Tests/Validator/Validator2Dot5ApiTest.php | 9 +++++++++ 13 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index fea3f7f1d7423..efcfbbaa3da0c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -12,30 +12,16 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface;; /** * @since %%NextVersion%% * @author Bernhard Schussek */ -interface ClassMetadataInterface extends MetadataInterface, ClassBasedInterface +interface ClassMetadataInterface extends MetadataInterface, LegacyPropertyMetadataContainerInterface, ClassBasedInterface { public function getConstrainedProperties(); - public function hasPropertyMetadata($property); - - /** - * 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); - public function hasGroupSequence(); public function getGroupSequence(); diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index a87903dec0258..47eda213a4109 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ValidationVisitorInterface; /** * @since %%NextVersion%% @@ -149,4 +150,12 @@ public function getTraversalStrategy() { return $this->traversalStrategy; } + + /** + * {@inheritdoc} + */ + public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath) + { + // Thanks PHP < 5.3.9 + } } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index f23bdef04c7dd..6aa0ead2ce683 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -13,11 +13,10 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; -use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, LegacyPropertyMetadataInterface +abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface { public $class; public $name; diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index 540c4c9d6064c..c533e4c2bd344 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; + /** * A container for validation metadata. * @@ -42,17 +44,8 @@ * * @author Bernhard Schussek */ -interface MetadataInterface +interface MetadataInterface extends LegacyMetadataInterface { - /** - * Returns all constraints for a given validation group. - * - * @param string $group The validation group. - * - * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. - */ - public function findConstraints($group); - public function getCascadingStrategy(); public function getTraversalStrategy(); diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php index 78da11b9074a9..138b1c9fb59b8 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; /** * A container for validation metadata of a property. @@ -26,21 +27,6 @@ * * @see MetadataInterface */ -interface PropertyMetadataInterface extends MetadataInterface, ClassBasedInterface +interface PropertyMetadataInterface extends MetadataInterface, LegacyPropertyMetadataInterface, ClassBasedInterface { - /** - * Returns the name of the property. - * - * @return string The property name. - */ - public function getPropertyName(); - - /** - * Extracts the value of the property from the given object. - * - * @param mixed $object The object to extract the property value from. - * - * @return mixed The value of the property. - */ - public function getPropertyValue($object); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php index 95851e8097ad7..151fca79f4b2d 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php @@ -22,7 +22,7 @@ class CountryValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - IntlTestHelper::requireIntl($this); + IntlTestHelper::requireFullIntl($this); $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new CountryValidator(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php index ea6c2eb43ac02..00b200e1e9463 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php @@ -22,7 +22,7 @@ class CurrencyValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - IntlTestHelper::requireIntl($this); + IntlTestHelper::requireFullIntl($this); $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new CurrencyValidator(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php index 3588887d74998..af5a05fbcceeb 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php @@ -22,7 +22,7 @@ class LanguageValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - IntlTestHelper::requireIntl($this); + IntlTestHelper::requireFullIntl($this); $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new LanguageValidator(); diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 45d3a70c12437..26fd879e5e7b5 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -46,6 +46,10 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase protected function setUp() { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + $this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface'); $this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface'); $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 4c830d25d4bff..11e4d31eb6366 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -35,17 +35,17 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase /** * @var FakeMetadataFactory */ - protected $metadataFactory; + public $metadataFactory; /** * @var ClassMetadata */ - protected $metadata; + public $metadata; /** * @var ClassMetadata */ - protected $referenceMetadata; + public $referenceMetadata; protected function setUp() { diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index e61daf9f939cd..7447c7ce29edf 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -23,6 +23,15 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest { + protected function setUp() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + parent::setUp(); + } + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 672f2ebf42c9a..e7fd2a8ccbc6c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -23,6 +23,15 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest { + protected function setUp() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + parent::setUp(); + } + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index b49c380c98708..a083903d53953 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -23,6 +23,15 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest { + protected function setUp() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + parent::setUp(); + } + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); From 2936d10aa4a630c279f0203f375daa2405a37f55 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:53:54 +0100 Subject: [PATCH 054/323] [Validator] Removed unused use statement --- .../Component/Validator/Tests/Context/ExecutionContextTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 26fd879e5e7b5..0f6e51f2ffc9e 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Tests\Context; use Symfony\Component\Validator\Context\ExecutionContext; -use Symfony\Component\Validator\Node\GenericNode; /** * @since 2.5 From e8fa15b27cf50af29fd1666371bfdcac851ee089 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 21:06:05 +0100 Subject: [PATCH 055/323] [Validator] Fixed the new validator API under PHP < 5.3.9 --- .../Validator/Context/ExecutionContext.php | 28 ++-- .../Context/ExecutionContextInterface.php | 122 +----------------- .../Context/LegacyExecutionContext.php | 7 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 9 -- 4 files changed, 22 insertions(+), 144 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index b58d92350e440..8f7c9ee6932f3 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -16,7 +16,6 @@ use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; -use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; @@ -33,7 +32,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeObserverInterface +class ExecutionContext implements ExecutionContextInterface, NodeObserverInterface { /** * @var ValidatorInterface @@ -121,6 +120,13 @@ public function addViolation($message, array $parameters = array(), $invalidValu // The parameters $invalidValue and following are ignored by the new // API, as they are not present in the new interface anymore. // You should use buildViolation() instead. + if (func_num_args() > 2) { + throw new BadMethodCallException( + 'The parameters $invalidValue, $pluralization and $code are '. + 'not supported anymore as of Symfony 2.5. Please use '. + 'buildViolation() instead or enable the legacy mode.' + ); + } $this->violations->add(new ConstraintViolation( $this->translator->trans($message, $parameters, $this->translationDomain), @@ -235,8 +241,8 @@ public function getPropertyPath($subPath = '') public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { throw new BadMethodCallException( - 'addViolationAt() is not supported anymore in the new API. '. - 'Please use buildViolation() or enable the legacy mode.' + 'addViolationAt() is not supported anymore as of Symfony 2.5. '. + 'Please use buildViolation() instead or enable the legacy mode.' ); } @@ -246,8 +252,8 @@ public function addViolationAt($subPath, $message, array $parameters = array(), public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { throw new BadMethodCallException( - 'validate() is not supported anymore in the new API. '. - 'Please use getValidator() or enable the legacy mode.' + 'validate() is not supported anymore as of Symfony 2.5. '. + 'Please use getValidator() instead or enable the legacy mode.' ); } @@ -257,8 +263,8 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals public function validateValue($value, $constraints, $subPath = '', $groups = null) { throw new BadMethodCallException( - 'validateValue() is not supported anymore in the new API. '. - 'Please use getValidator() or enable the legacy mode.' + 'validateValue() is not supported anymore as of Symfony 2.5. '. + 'Please use getValidator() instead or enable the legacy mode.' ); } @@ -268,9 +274,9 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul public function getMetadataFactory() { throw new BadMethodCallException( - 'getMetadataFactory() is not supported anymore in the new API. '. - 'Please use getMetadataFor() or hasMetadataFor() or enable the '. - 'legacy mode.' + 'getMetadataFactory() is not supported anymore as of Symfony 2.5. '. + 'Please use getValidator() in combination with getMetadataFor() '. + 'or hasMetadataFor() instead or enable the legacy mode.' ); } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index cbbc3c7447112..4fda437f6cb8f 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -59,16 +60,8 @@ * @since 2.5 * @author Bernhard Schussek */ -interface ExecutionContextInterface +interface ExecutionContextInterface extends LegacyExecutionContextInterface { - /** - * Adds a violation at the current node of the validation graph. - * - * @param string $message The error message - * @param array $parameters The parameters substituted in the error message - */ - public function addViolation($message, array $parameters = array()); - /** * Returns a builder for adding a violation with extended information. * @@ -88,13 +81,6 @@ public function addViolation($message, array $parameters = array()); */ public function buildViolation($message, array $parameters = array()); - /** - * Returns the violations generated in this context. - * - * @return ConstraintViolationListInterface The constraint violations - */ - public function getViolations(); - /** * Returns the validator. * @@ -114,108 +100,4 @@ public function getViolations(); * @return ValidatorInterface */ public function getValidator(); - - /** - * Returns the root value of 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 at which the traversal started. - * - * The current value is returned by {@link getValue()}. - * - * @return mixed|null The root value of the validation or null, if no value - * is currently being validated - */ - 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|null The currently validated value or null, if no value is - * currently being validated - */ - 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. The metadata can - * also be an {@link Mapping\GenericMetadata} if the current value does not - * belong to any structural element. - * - * @return MetadataInterface|null The metadata of the currently validated - * value or null, if no value is currently - * being validated - */ - public function getMetadata(); - - /** - * Returns the validation group that is currently being validated. - * - * @return string|GroupSequence|null The current validation group or null, - * if no value is currently being - * validated - */ - public function getGroup(); - - /** - * Returns the class name of the current node. - * - * If the metadata of the current node does not implement - * {@link ClassBasedInterface}, 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}, 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 instances, 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 = ''); } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 7e3b5971aacdb..f8ab1c9981fff 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -15,7 +15,6 @@ use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -29,7 +28,7 @@ * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be * removed in 3.0. */ -class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface +class LegacyExecutionContext extends ExecutionContext { /** * Creates a new context. @@ -66,7 +65,7 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn */ public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { - if (func_num_args() >= 3) { + if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->setInvalidValue($invalidValue) @@ -86,7 +85,7 @@ public function addViolation($message, array $parameters = array(), $invalidValu */ public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { - if (func_num_args() >= 3) { + if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->atPath($subPath) diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index a083903d53953..b49c380c98708 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -23,15 +23,6 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest { - protected function setUp() - { - if (version_compare(PHP_VERSION, '5.3.9', '<')) { - $this->markTestSkipped('Not supported prior to PHP 5.3.9'); - } - - parent::setUp(); - } - protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); From 85583779450d3d944e26baa967713a94af61252e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 21:25:19 +0100 Subject: [PATCH 056/323] [Validator] Added deprecation notes --- .../Validator/ClassBasedInterface.php | 3 +++ .../Validator/Constraints/GroupSequence.php | 12 ++++----- .../Context/LegacyExecutionContext.php | 4 +-- .../Context/LegacyExecutionContextFactory.php | 4 +-- .../Component/Validator/ExecutionContext.php | 3 +++ .../Validator/ExecutionContextInterface.php | 25 +++++++++++++++++++ .../GlobalExecutionContextInterface.php | 3 +++ .../Component/Validator/MetadataInterface.php | 5 ++++ .../PropertyMetadataContainerInterface.php | 3 +++ .../Validator/PropertyMetadataInterface.php | 3 +++ .../Component/Validator/ValidationVisitor.php | 3 +++ .../Validator/ValidationVisitorInterface.php | 7 ++++++ src/Symfony/Component/Validator/Validator.php | 3 +++ .../Validator/Validator/LegacyValidator.php | 5 +++- .../Validator/ValidatorInterface.php | 15 +++++++++++ 15 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Validator/ClassBasedInterface.php b/src/Symfony/Component/Validator/ClassBasedInterface.php index c8fa25d43d796..fe532efe51702 100644 --- a/src/Symfony/Component/Validator/ClassBasedInterface.php +++ b/src/Symfony/Component/Validator/ClassBasedInterface.php @@ -15,6 +15,9 @@ * An object backed by a PHP class. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\ClassMetadataInterface} instead. */ interface ClassBasedInterface { diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 10dec2ef83f7b..a2406c7b6e563 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -99,7 +99,7 @@ public function __construct(array $groups) * @see \IteratorAggregate::getIterator() * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function getIterator() { @@ -114,7 +114,7 @@ public function getIterator() * @return Boolean Whether the offset exists * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetExists($offset) { @@ -131,7 +131,7 @@ public function offsetExists($offset) * @throws OutOfBoundsException If the object does not exist * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetGet($offset) { @@ -152,7 +152,7 @@ public function offsetGet($offset) * @param string $value The group name * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetSet($offset, $value) { @@ -171,7 +171,7 @@ public function offsetSet($offset, $value) * @param integer $offset The offset * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetUnset($offset) { @@ -184,7 +184,7 @@ public function offsetUnset($offset) * @return integer The number of groups * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function count() { diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index f8ab1c9981fff..b84d1c379c22f 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -25,8 +25,8 @@ * @since 2.5 * @author Bernhard Schussek * - * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be - * removed in 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in Symfony 3.0. */ class LegacyExecutionContext extends ExecutionContext { diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index 181b4f47fd00d..7f19bb204c02b 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -21,8 +21,8 @@ * @since 2.5 * @author Bernhard Schussek * - * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be - * removed in 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in Symfony 3.0. */ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface { diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 00b8cca6369ee..6435bbf9d0568 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -20,6 +20,9 @@ * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContext} instead. */ class ExecutionContext implements ExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php index 92f4c5690b0af..b89caa2deaee2 100644 --- a/src/Symfony/Component/Validator/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -82,6 +82,9 @@ * @author Bernhard Schussek * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface} instead. */ interface ExecutionContextInterface { @@ -95,6 +98,10 @@ interface ExecutionContextInterface * @param integer|null $code The violation code. * * @api + * + * @deprecated The parameters $invalidValue, $pluralization and $code are + * deprecated since version 2.5 and will be removed in + * Symfony 3.0. */ public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); @@ -110,6 +117,10 @@ public function addViolation($message, array $params = array(), $invalidValue = * @param integer|null $code The violation code. * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::buildViolation()} + * instead. */ public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null); @@ -151,6 +162,10 @@ public function addViolationAt($subPath, $message, array $parameters = array(), * or an instance of \Traversable. * @param Boolean $deep Whether to traverse the value recursively if * it is a collection of collections. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::getValidator()} + * instead. */ public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false); @@ -180,6 +195,10 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals * @param null|string|string[] $groups The groups to validate in. If you don't pass any * groups here, the current group of the context * will be used. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::getValidator()} + * instead. */ public function validateValue($value, $constraints, $subPath = '', $groups = null); @@ -237,6 +256,12 @@ public function getMetadata(); * Returns the used metadata factory. * * @return MetadataFactoryInterface The metadata factory. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::getValidator()} + * instead and call + * {@link Validator\ValidatorInterface::getMetadataFor()} or + * {@link Validator\ValidatorInterface::hasMetadataFor()} there. */ public function getMetadataFactory(); diff --git a/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php b/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php index aff44b350769f..fb2aef3bd772d 100644 --- a/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php @@ -26,6 +26,9 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface} instead. */ interface GlobalExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/MetadataInterface.php b/src/Symfony/Component/Validator/MetadataInterface.php index a5d65048b7816..b2cb20e847b33 100644 --- a/src/Symfony/Component/Validator/MetadataInterface.php +++ b/src/Symfony/Component/Validator/MetadataInterface.php @@ -41,6 +41,9 @@ * again. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\MetadataInterface} instead. */ interface MetadataInterface { @@ -54,6 +57,8 @@ interface MetadataInterface * @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. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath); diff --git a/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php b/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php index 20bafb2950fd9..2bb00f2a9ab96 100644 --- a/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php +++ b/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php @@ -15,6 +15,9 @@ * A container for {@link PropertyMetadataInterface} instances. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\ClassMetadataInterface} instead. */ interface PropertyMetadataContainerInterface { diff --git a/src/Symfony/Component/Validator/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/PropertyMetadataInterface.php index eaac1a7121103..c18ae83a05e81 100644 --- a/src/Symfony/Component/Validator/PropertyMetadataInterface.php +++ b/src/Symfony/Component/Validator/PropertyMetadataInterface.php @@ -23,6 +23,9 @@ * @author Bernhard Schussek * * @see MetadataInterface + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\PropertyMetadataInterface} instead. */ interface PropertyMetadataInterface extends MetadataInterface { diff --git a/src/Symfony/Component/Validator/ValidationVisitor.php b/src/Symfony/Component/Validator/ValidationVisitor.php index ddff8adc60382..302d33c9d7f43 100644 --- a/src/Symfony/Component/Validator/ValidationVisitor.php +++ b/src/Symfony/Component/Validator/ValidationVisitor.php @@ -20,6 +20,9 @@ * {@link GlobalExecutionContextInterface}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link NodeVisitor\NodeVisitorInterface} instead. */ class ValidationVisitor implements ValidationVisitorInterface, GlobalExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/ValidationVisitorInterface.php b/src/Symfony/Component/Validator/ValidationVisitorInterface.php index e4163718b30d0..513775b31f1cc 100644 --- a/src/Symfony/Component/Validator/ValidationVisitorInterface.php +++ b/src/Symfony/Component/Validator/ValidationVisitorInterface.php @@ -33,6 +33,9 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link NodeVisitor\NodeVisitorInterface} instead. */ interface ValidationVisitorInterface { @@ -62,6 +65,8 @@ interface ValidationVisitorInterface * * @throws Exception\NoSuchMetadataException If no metadata can be found for * the given value. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ public function validate($value, $group, $propertyPath, $traverse = false, $deep = false); @@ -75,6 +80,8 @@ public function validate($value, $group, $propertyPath, $traverse = false, $deep * @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. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ public function visit(MetadataInterface $metadata, $value, $group, $propertyPath); } diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index a7bcc3a0a6cfb..e80157db8123c 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -20,6 +20,9 @@ * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Validator\Validator} instead. */ class Validator implements ValidatorInterface { diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 5bc570737625e..9ecf522f85633 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -17,8 +17,11 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** - * @since %%NextVersion%% + * @since 2.5 * @author Bernhard Schussek + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in Symfony 3.0. */ class LegacyValidator extends Validator implements LegacyValidatorInterface { diff --git a/src/Symfony/Component/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/ValidatorInterface.php index 98e02d90cf4d8..062b1adc9f789 100644 --- a/src/Symfony/Component/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/ValidatorInterface.php @@ -17,6 +17,9 @@ * @author Bernhard Schussek * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Validator\ValidatorInterface} instead. */ interface ValidatorInterface { @@ -35,6 +38,10 @@ interface ValidatorInterface * list is empty, validation succeeded. * * @api + * + * @deprecated The signature changed with Symfony 2.5 (see + * {@link Validator\ValidatorInterface::validate()}. This + * signature will be disabled in Symfony 3.0. */ public function validate($value, $groups = null, $traverse = false, $deep = false); @@ -85,6 +92,9 @@ public function validatePropertyValue($containingValue, $property, $value, $grou * list is empty, validation succeeded. * * @api + * + * @deprecated Renamed to {@link Validator\ValidatorInterface::validate()} + * in Symfony 2.5. Will be removed in Symfony 3.0. */ public function validateValue($value, $constraints, $groups = null); @@ -94,6 +104,11 @@ public function validateValue($value, $constraints, $groups = null); * @return MetadataFactoryInterface The metadata factory. * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Validator\ValidatorInterface::getMetadataFor()} or + * {@link Validator\ValidatorInterface::hasMetadataFor()} + * instead. */ public function getMetadataFactory(); } From dbce5a2f6ae60d47f029314a41583fa1a3de685a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 21:27:30 +0100 Subject: [PATCH 057/323] [Validator] Updated outdated doc blocks --- .../Component/Validator/Constraints/GroupSequence.php | 6 +++--- .../Validator/Context/ExecutionContextInterface.php | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index a2406c7b6e563..af3b86c1c6fb7 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -19,7 +19,7 @@ * When validating a group sequence, each group will only be validated if all * of the previous groups in the sequence succeeded. For example: * - * $validator->validateObject($address, new GroupSequence('Basic', 'Strict')); + * $validator->validate($address, new Valid(), new GroupSequence('Basic', 'Strict')); * * In the first step, all constraints that belong to the group "Basic" will be * validated. If none of the constraints fail, the validator will then validate @@ -42,12 +42,12 @@ * Whenever you validate that object in the "Default" group, the group sequence * will be validated: * - * $validator->validateObject($address); + * $validator->validate($address, new Valid()); * * If you want to execute the constraints of the "Default" group for a class * with an overridden default group, pass the class name as group name instead: * - * $validator->validateObject($address, "Address") + * $validator->validate($address, new Valid(), "Address") * * @Annotation * diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 4fda437f6cb8f..f6fed2fbee062 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -11,10 +11,7 @@ namespace Symfony\Component\Validator\Context; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; -use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -24,7 +21,7 @@ * The context collects all violations generated during the validation. By * default, validators execute all validations in a new context: * - * $violations = $validator->validateObject($object); + * $violations = $validator->validate($object); * * When you make another call to the validator, while the validation is in * progress, the violations will be isolated from each other: @@ -34,7 +31,7 @@ * $validator = $this->context->getValidator(); * * // The violations are not added to $this->context - * $violations = $validator->validateObject($value); + * $violations = $validator->validate($value); * } * * However, if you want to add the violations to the current context, use the @@ -47,7 +44,7 @@ * // The violations are added to $this->context * $validator * ->inContext($this->context) - * ->validateObject($value) + * ->validate($value) * ; * } * From 822fe4712f4c4173c1fdf6f4fa286aec319e2e2a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 13:47:18 +0100 Subject: [PATCH 058/323] [Validator] Completed inline documentation of the Node classes and the NodeTraverser --- .../Component/Validator/Node/ClassNode.php | 34 +- .../Validator/Node/CollectionNode.php | 12 +- .../Component/Validator/Node/GenericNode.php | 3 +- src/Symfony/Component/Validator/Node/Node.php | 6 +- .../Component/Validator/Node/PropertyNode.php | 17 +- .../NodeTraverser/NodeTraverserInterface.php | 70 +++- ...rser.php => NonRecursiveNodeTraverser.php} | 358 +++++++++++++----- .../Validator/LegacyValidator2Dot5ApiTest.php | 4 +- .../LegacyValidatorLegacyApiTest.php | 4 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 4 +- 10 files changed, 382 insertions(+), 130 deletions(-) rename src/Symfony/Component/Validator/NodeTraverser/{NodeTraverser.php => NonRecursiveNodeTraverser.php} (52%) diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 7c82eead9adec..54e22e2d97403 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -18,6 +18,13 @@ /** * Represents an object and its class metadata in the validation graph. * + * If the object is a collection which should be traversed, a new + * {@link CollectionNode} instance will be created for that object: + * + * (TagList:ClassNode) + * \ + * (TagList:CollectionNode) + * * @since 2.5 * @author Bernhard Schussek */ @@ -31,19 +38,22 @@ class ClassNode extends Node /** * Creates a new class node. * - * @param object $object The validated object - * @param ClassMetadataInterface $metadata The class metadata of that - * object - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should be - * validated - * @param integer $traversalStrategy + * @param object $object The validated object + * @param ClassMetadataInterface $metadata The class metadata of + * that object + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should + * be validated + * @param integer $traversalStrategy The strategy used for + * traversing the object + * + * @throws UnexpectedTypeException If $object is not an object * - * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index 763ed63072c00..ddca97a5c7f09 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -15,7 +15,7 @@ use Symfony\Component\Validator\Mapping\TraversalStrategy; /** - * Represents an traversable collection in the validation graph. + * Represents a traversable value in the validation graph. * * @since 2.5 * @author Bernhard Schussek @@ -33,13 +33,19 @@ class CollectionNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated - * @param integer $traversalStrategy The traversal strategy + * @param integer $traversalStrategy The strategy used for + * traversing the collection * - * @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException + * @throws ConstraintDefinitionException If $collection is not an array or a + * \Traversable + * + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE) { if (!is_array($collection) && !$collection instanceof \Traversable) { + // Must throw a ConstraintDefinitionException for backwards + // compatibility reasons with Symfony < 2.5 throw new ConstraintDefinitionException(sprintf( 'Traversal was enabled for "%s", but this class '. 'does not implement "\Traversable".', diff --git a/src/Symfony/Component/Validator/Node/GenericNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php index 82ee9ac7fbaad..9c628e0a465e0 100644 --- a/src/Symfony/Component/Validator/Node/GenericNode.php +++ b/src/Symfony/Component/Validator/Node/GenericNode.php @@ -16,8 +16,7 @@ * attached to it. * * Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, - * this node type can be used to validate a value against some given - * constraints. + * this node type can be used to validate a value against some constraints. * * @since 2.5 * @author Bernhard Schussek diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 038217bf512f9..56c8145c45682 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Mapping\TraversalStrategy; /** - * A node in the validated graph. + * A node in the validation graph. * * @since 2.5 * @author Bernhard Schussek @@ -59,7 +59,11 @@ abstract class Node public $cascadedGroups; /** + * The strategy used for traversing the validated value. + * * @var integer + * + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public $traversalStrategy; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 14dedb320356e..8934bf1d7330c 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -19,16 +19,23 @@ * Represents the value of a property and its associated metadata. * * If the property contains an object and should be cascaded, a new - * {@link ClassNode} instance will be created for that object. - * - * Example: + * {@link ClassNode} instance will be created for that object: * * (Article:ClassNode) * \ - * (author:PropertyNode) + * (->author:PropertyNode) * \ * (Author:ClassNode) * + * If the property contains a collection which should be traversed, a new + * {@link CollectionNode} instance will be created for that collection: + * + * (Article:ClassNode) + * \ + * (->tags:PropertyNode) + * \ + * (array:CollectionNode) + * * @since 2.5 * @author Bernhard Schussek */ @@ -61,6 +68,8 @@ class PropertyNode extends Node * @param integer $traversalStrategy * * @throws UnexpectedTypeException If $object is not an object + * + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 6084403d9bb16..37c7c8c22c519 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -12,17 +12,85 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** - * @since %%NextVersion%% + * Traverses the nodes of the validation graph. + * + * You can attach visitors to the traverser that are invoked during the + * traversal. Before starting the traversal, the + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} + * method of each visitor is called. For each node in the graph, the + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()} + * of each visitor is called. At the end of the traversal, the traverser invokes + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} + * on each visitor. + * + * The visitors should be called in the same order in which they are added to + * the traverser. + * + * The validation graph typically contains nodes of the following types: + * + * - {@link \Symfony\Component\Validator\Node\ClassNode}: + * An object with associated class metadata + * - {@link \Symfony\Component\Validator\Node\PropertyNode}: + * A property value with associated property metadata + * - {@link \Symfony\Component\Validator\Node\GenericNode}: + * A generic value with associated constraints + * - {@link \Symfony\Component\Validator\Node\CollectionNode}: + * A traversable collection + * + * Generic nodes are mostly useful when you want to validate a value that has + * neither associated class nor property metadata. Generic nodes usually come + * with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, that + * contains the constraints that the value should be validated against. + * + * Whenever a class, property or generic node is validated that contains a + * traversable value which should be traversed (according to the + * {@link \Symfony\Component\Validator\Mapping\TraversalStrategy} specified + * in the node or its metadata), a new + * {@link \Symfony\Component\Validator\Node\CollectionNode} will be attached + * to the node graph. + * + * For example: + * + * (TagList:ClassNode) + * \ + * (TagList:CollectionNode) + * + * When writing custom visitors, be aware that collection nodes usually contain + * values that have already been passed to the visitor before through a class + * node, a property node or a generic node. + * + * @since 2.5 * @author Bernhard Schussek */ interface NodeTraverserInterface { + /** + * Adds a new visitor to the traverser. + * + * Visitors that have already been added before are ignored. + * + * @param NodeVisitorInterface $visitor The visitor to add + */ public function addVisitor(NodeVisitorInterface $visitor); + /** + * Removes a visitor from the traverser. + * + * Non-existing visitors are ignored. + * + * @param NodeVisitorInterface $visitor The visitor to remove + */ public function removeVisitor(NodeVisitorInterface $visitor); + /** + * Traverses the given nodes in the given context. + * + * @param Node[] $nodes The nodes to traverse + * @param ExecutionContextInterface $context The validation context + */ public function traverse(array $nodes, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php similarity index 52% rename from src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php rename to src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index be9f2d2b880e8..ca1e2a9db5adc 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -24,10 +24,37 @@ use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** - * @since %%NextVersion%% + * Non-recursive implementation of {@link NodeTraverserInterface}. + * + * This implementation uses a Depth First algorithm to traverse the node + * graph. Instead of loading the complete node graph into memory before the + * traversal, the traverser only expands the successor nodes of a node once + * that node is traversed. For example, when traversing a class node, the + * nodes for all constrained properties of that class are loaded into memory. + * When the traversal of the class node is over, the node is discarded. + * + * Next, one of the class' property nodes is traversed. At that point, the + * successor nodes of that property node (a class node, if the property should + * be cascaded, or a collection node, if the property should be traversed) are + * loaded into memory. As soon as the traversal of the property node is over, + * it is discarded as well. + * + * This leads to an average memory consumption of O(log N * B), where N is the + * number of nodes in the graph and B is the average number of successor nodes + * of a node. + * + * In order to maintain a small execution stack, nodes are not validated + * recursively, but iteratively. Internally, a stack is used to store all the + * nodes that should be processed. Whenever a node is traversed, its successor + * nodes are put on the stack. The traverser keeps fetching and traversing nodes + * from the stack until the stack is empty and all nodes have been traversed. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see NodeTraverserInterface */ -class NodeTraverser implements NodeTraverserInterface +class NonRecursiveNodeTraverser implements NodeTraverserInterface { /** * @var NodeVisitorInterface[] @@ -39,20 +66,34 @@ class NodeTraverser implements NodeTraverserInterface */ private $metadataFactory; + /** + * @var Boolean + */ private $traversalStarted = false; + /** + * Creates a new traverser. + * + * @param MetadataFactoryInterface $metadataFactory The metadata factory + */ public function __construct(MetadataFactoryInterface $metadataFactory) { $this->visitors = new \SplObjectStorage(); - $this->nodeQueue = new \SplQueue(); + $this->nodeStack = new \SplStack(); $this->metadataFactory = $metadataFactory; } + /** + * {@inheritdoc} + */ public function addVisitor(NodeVisitorInterface $visitor) { $this->visitors->attach($visitor); } + /** + * {@inheritdoc} + */ public function removeVisitor(NodeVisitorInterface $visitor) { $this->visitors->detach($visitor); @@ -63,45 +104,67 @@ public function removeVisitor(NodeVisitorInterface $visitor) */ public function traverse(array $nodes, ExecutionContextInterface $context) { + // beforeTraversal() and afterTraversal() are only executed for the + // top-level call of traverse() $isTopLevelCall = !$this->traversalStarted; if ($isTopLevelCall) { + // Remember that the traversal was already started for the case of + // recursive calls to traverse() $this->traversalStarted = true; foreach ($this->visitors as $visitor) { - /** @var NodeVisitorInterface $visitor */ $visitor->beforeTraversal($nodes, $context); } } - $nodeQueue = new \SplQueue(); + // This stack contains all the nodes that should be traversed + // A stack is used rather than a queue in order to traverse the graph + // in a Depth First approach (the last added node is processed first). + // In this way, the order in which the nodes are passed to the visitors + // is similar to a recursive implementation (except that the successor + // nodes of a node are traversed right-to-left instead of left-to-right). + $nodeStack = new \SplStack(); foreach ($nodes as $node) { - $nodeQueue->enqueue($node); + // Push a node to the stack and immediately process it. This way, + // the successor nodes are traversed before the next node in $nodes + $nodeStack->push($node); - while (!$nodeQueue->isEmpty()) { - $node = $nodeQueue->dequeue(); + // Fetch nodes from the stack and traverse them until no more nodes + // are left. Then continue with the next node in $nodes. + while (!$nodeStack->isEmpty()) { + $node = $nodeStack->pop(); if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $nodeQueue, $context); + $this->traverseClassNode($node, $context, $nodeStack); } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $nodeQueue, $context); + $this->traverseCollectionNode($node, $context, $nodeStack); } else { - $this->traverseNode($node, $nodeQueue, $context); + $this->traverseNode($node, $context, $nodeStack); } } } if ($isTopLevelCall) { foreach ($this->visitors as $visitor) { - /** @var NodeVisitorInterface $visitor */ $visitor->afterTraversal($nodes, $context); } + // Put the traverser back into its initial state $this->traversalStarted = false; } } + /** + * Executes the {@link NodeVisitorInterface::visit()} method of each + * visitor. + * + * @param Node $node The visited node + * @param ExecutionContextInterface $context The current execution context + * + * @return Boolean Whether to traverse the node's successor nodes + */ private function visit(Node $node, ExecutionContextInterface $context) { foreach ($this->visitors as $visitor) { @@ -113,87 +176,27 @@ private function visit(Node $node, ExecutionContextInterface $context) return true; } - private function traverseNode(Node $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (null === $node->value) { - return; - } - - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = null !== $node->cascadedGroups - ? $node->cascadedGroups - : $node->groups; - - if (0 === count($cascadedGroups)) { - return; - } - - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->metadata->getTraversalStrategy(); - - if (is_array($node->value)) { - // Arrays are always traversed, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $nodeQueue->enqueue(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); - - return; - } - - if ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $node->value, - $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $nodeQueue - ); - - return; - } - - // Traverse only if IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - return; - } - - // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { - return; - } - - // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeQueue->enqueue(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); - } - - private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) + /** + * Traverses a class node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, a property + * node is put on the node stack for each constrained property of the class. + * At last, if the class is traversable and should be traversed according + * to the selected traversal strategy, a new collection node is put on the + * stack. + * + * @param ClassNode $node The class node + * @param ExecutionContextInterface $context The current execution context + * @param \SplStack $nodeStack The stack for storing the + * successor nodes + * + * @see ClassNode + * @see PropertyNode + * @see CollectionNode + * @see TraversalStrategy + */ + private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) { // Visitors have two possibilities to influence the traversal: // @@ -212,7 +215,7 @@ private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, Execut foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $nodeQueue->enqueue(new PropertyNode( + $nodeStack->push(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), $propertyMetadata, @@ -246,7 +249,7 @@ private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, Execut } // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeQueue->enqueue(new CollectionNode( + $nodeStack->push(new CollectionNode( $node->value, $node->propertyPath, $node->groups, @@ -255,7 +258,31 @@ private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, Execut )); } - private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) + /** + * Traverses a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - for each object in the collection with associated class metadata, a + * new class node is put on the stack; + * - if an object has no associated class metadata, but is traversable, and + * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for + * collection node, a new collection node is put on the stack for that + * object; + * - for each array in the collection, a new collection node is put on the + * stack. + * + * @param CollectionNode $node The collection node + * @param ExecutionContextInterface $context The current execution context + * @param \SplStack $nodeStack The stack for storing the + * successor nodes + * + * @see ClassNode + * @see CollectionNode + */ + private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) { // Visitors have two possibilities to influence the traversal: // @@ -285,7 +312,7 @@ private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQue // Arrays are always cascaded, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $nodeQueue->enqueue(new CollectionNode( + $nodeStack->push(new CollectionNode( $value, $node->propertyPath.'['.$key.']', $node->groups, @@ -304,13 +331,142 @@ private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQue $node->propertyPath.'['.$key.']', $node->groups, $traversalStrategy, - $nodeQueue + $nodeStack ); } } } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplQueue $nodeQueue) + /** + * Traverses a node that is neither a class nor a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - if the node contains an object with associated class metadata, a new + * class node is put on the stack; + * - if the node contains a traversable object without associated class + * metadata and traversal is enabled according to the selected traversal + * strategy, a collection node is put on the stack; + * - if the node contains an array, a collection node is put on the stack. + * + * @param Node $node The node + * @param ExecutionContextInterface $context The current execution context + * @param \SplStack $nodeStack The stack for storing the + * successor nodes + */ + private function traverseNode(Node $node, ExecutionContextInterface $context, \SplStack $nodeStack) + { + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's visit() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's visit() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $this->visit($node, $context)) { + return; + } + + if (null === $node->value) { + return; + } + + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->traversalStrategy; + + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the node's metadata + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $nodeStack->push(new CollectionNode( + $node->value, + $node->propertyPath, + $cascadedGroups, + null, + $traversalStrategy + )); + + return; + } + + if ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $node->value, + $node->propertyPath, + $cascadedGroups, + $traversalStrategy, + $nodeStack + ); + + return; + } + + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + return; + } + + // If IMPLICIT, stop unless we deal with a Traversable + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + return; + } + + // If TRAVERSE, the constructor will fail if we have no Traversable + $nodeStack->push(new CollectionNode( + $node->value, + $node->propertyPath, + $cascadedGroups, + null, + $traversalStrategy + )); + } + + /** + * Executes the cascading logic for an object. + * + * If class metadata is available for the object, a class node is put on + * the node stack. Otherwise, if the selected traversal strategy allows + * traversal of the object, a new collection node is put on the stack. + * Otherwise, an exception is thrown. + * + * @param object $object The object to cascade + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param integer $traversalStrategy The strategy for traversing the + * cascaded object + * @param \SplStack $nodeStack The stack for storing the successor + * nodes + * + * @throws NoSuchMetadataException If the object has no associated metadata + * and does not implement {@link \Traversable} + * or if traversal is disabled via the + * $traversalStrategy argument + * + */ + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -319,7 +475,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal // error } - $nodeQueue->enqueue(new ClassNode( + $nodeStack->push(new ClassNode( $object, $classMetadata, $propertyPath, @@ -338,7 +494,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal throw $e; } - $nodeQueue->enqueue(new CollectionNode( + $nodeStack->push(new CollectionNode( $object, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 7447c7ce29edf..5468642e7d3d3 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index e7fd2a8ccbc6c..3edd86b41054f 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index b49c380c98708..277d641b7aee4 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -18,14 +18,14 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\Validator; class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); From 186c115894737c0934b830910b3ec222f94bb789 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 14:44:01 +0100 Subject: [PATCH 059/323] [Validator] Improved test coverage of NonRecursiveNodeTraverser --- .../UnsupportedMetadataException.php | 20 +++++ .../Validator/Mapping/ClassMetadata.php | 11 +++ .../Validator/Mapping/GenericMetadata.php | 78 ++++++++++++++++--- .../NonRecursiveNodeTraverser.php | 47 ++++++----- .../Tests/Fixtures/FakeClassMetadata.php | 29 +++++++ .../Tests/Fixtures/FakeMetadataFactory.php | 17 +++- .../Tests/Fixtures/LegacyClassMetadata.php | 20 +++++ .../Tests/Validator/Abstract2Dot5ApiTest.php | 55 +++++++++++++ 8 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php diff --git a/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php b/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php new file mode 100644 index 0000000000000..c6ece50b70062 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Exception; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class UnsupportedMetadataException extends InvalidArgumentException +{ +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 5a2782f854041..30c7b1ad3b04c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -64,6 +64,15 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, */ public $groupSequenceProvider = false; + /** + * The strategy for traversing traversable objects. + * + * By default, only instances of {@link \Traversable} are traversed. + * + * @var integer + */ + public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** * @var \ReflectionClass */ @@ -215,6 +224,8 @@ public function addConstraint(Constraint $constraint) $constraint->addImplicitGroupName($this->getDefaultGroup()); parent::addConstraint($constraint); + + return $this; } /** diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 47eda213a4109..f77e11736a464 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -13,10 +13,13 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\ValidationVisitorInterface; /** - * @since %%NextVersion%% + * A generic container of {@link Constraint} objects. + * + * @since 2.5 * @author Bernhard Schussek */ class GenericMetadata implements MetadataInterface @@ -31,9 +34,27 @@ class GenericMetadata implements MetadataInterface */ public $constraintsByGroup = array(); + /** + * The strategy for cascading objects. + * + * By default, objects are not cascaded. + * + * @var integer + * + * @see CascadingStrategy + */ public $cascadingStrategy = CascadingStrategy::NONE; - public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** + * The strategy for traversing traversable objects. + * + * By default, traversable objects are not traversed. + * + * @var integer + * + * @see TraversalStrategy + */ + public $traversalStrategy = TraversalStrategy::NONE; /** * Returns the names of the properties that should be serialized. @@ -66,11 +87,22 @@ public function __clone() } /** - * Adds a constraint to this element. + * Adds a constraint. + * + * If the constraint {@link Valid} is added, the cascading strategy will be + * changed to {@link CascadingStrategy::CASCADE}. Depending on the + * properties $traverse and $deep of that constraint, the traversal strategy + * will be set to one of the following: * - * @param Constraint $constraint + * - {@link TraversalStrategy::IMPLICIT} if $traverse is enabled and $deep + * is enabled + * - {@link TraversalStrategy::IMPLICIT} | {@link TraversalStrategy::STOP_RECURSION} + * if $traverse is enabled, but $deep is disabled + * - {@link TraversalStrategy::NONE} if $traverse is disabled * - * @return ElementMetadata + * @param Constraint $constraint The constraint to add + * + * @return GenericMetadata This object */ public function addConstraint(Constraint $constraint) { @@ -100,17 +132,26 @@ public function addConstraint(Constraint $constraint) return $this; } + /** + * Adds an list of constraints. + * + * @param Constraint[] $constraints The constraints to add + * + * @return GenericMetadata This object + */ public function addConstraints(array $constraints) { foreach ($constraints as $constraint) { $this->addConstraint($constraint); } + + return $this; } /** * Returns all constraints of this element. * - * @return Constraint[] An array of Constraint instances + * @return Constraint[] A list of Constraint instances */ public function getConstraints() { @@ -132,30 +173,45 @@ public function hasConstraints() * * @param string $group The group name * - * @return array An array with all Constraint instances belonging to the group + * @return Constraint[] An list of all the Constraint instances belonging + * to the group */ public function findConstraints($group) { return isset($this->constraintsByGroup[$group]) - ? $this->constraintsByGroup[$group] - : array(); + ? $this->constraintsByGroup[$group] + : array(); } + /** + * {@inheritdoc} + */ public function getCascadingStrategy() { return $this->cascadingStrategy; } + /** + * {@inheritdoc} + */ public function getTraversalStrategy() { return $this->traversalStrategy; } /** - * {@inheritdoc} + * Exists for compatibility with the deprecated + * {@link Symfony\Component\Validator\MetadataInterface}. + * + * Should not be used. + * + * @throws BadMethodCallException + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * Will be removed in Symfony 3.0. */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath) { - // Thanks PHP < 5.3.9 + throw new BadMethodCallException('Not supported.'); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index ca1e2a9db5adc..b931b969431ad 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -13,8 +13,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -191,6 +193,9 @@ private function visit(Node $node, ExecutionContextInterface $context) * @param \SplStack $nodeStack The stack for storing the * successor nodes * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * * @see ClassNode * @see PropertyNode * @see CollectionNode @@ -215,6 +220,15 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + if (!$propertyMetadata instanceof PropertyMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The property metadata instances should implement '. + '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. + 'got: "%s".', + is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) + )); + } + $nodeStack->push(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), @@ -424,24 +438,12 @@ private function traverseNode(Node $node, ExecutionContextInterface $context, \S return; } - // Traverse only if IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - return; - } - - // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { - return; - } + // Currently, the traversal strategy can only be TRAVERSE for a + // generic node if the cascading strategy is CASCADE. Thus, traversable + // objects will always be handled within cascadeObject() and there's + // nothing more to do here. - // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); + // see GenericMetadata::addConstraint() } /** @@ -464,7 +466,9 @@ private function traverseNode(Node $node, ExecutionContextInterface $context, \S * and does not implement {@link \Traversable} * or if traversal is disabled via the * $traversalStrategy argument - * + * @throws UnsupportedMetadataException If the metadata returned by the + * metadata factory does not implement + * {@link ClassMetadataInterface} */ private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) { @@ -472,7 +476,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); } $nodeStack->push(new ClassNode( diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php new file mode 100644 index 0000000000000..c6b79f66f336d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php @@ -0,0 +1,29 @@ + + * + * 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\ClassBasedInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; + +class FakeClassMetadata extends ClassMetadata +{ + public function addPropertyMetadata($propertyName, $metadata) + { + if (!isset($this->members[$propertyName])) { + $this->members[$propertyName] = array(); + } + + $this->members[$propertyName][] = $metadata; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index b03eacf71e90a..09b0ca63bea53 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -22,7 +22,10 @@ class FakeMetadataFactory implements MetadataFactoryInterface public function getMetadataFor($class) { + $hash = null; + if (is_object($class)) { + $hash = spl_object_hash($class); $class = get_class($class); } @@ -31,6 +34,10 @@ public function getMetadataFor($class) } if (!isset($this->metadatas[$class])) { + if (isset($this->metadatas[$hash])) { + return $this->metadatas[$hash]; + } + throw new NoSuchMetadataException(sprintf('No metadata for "%s"', $class)); } @@ -39,24 +46,28 @@ public function getMetadataFor($class) public function hasMetadataFor($class) { + $hash = null; + if (is_object($class)) { $class = get_class($class); + $hash = spl_object_hash($hash); } if (!is_string($class)) { return false; } - return isset($this->metadatas[$class]); + return isset($this->metadatas[$class]) || isset($this->metadatas[$hash]); } - public function addMetadata(ClassMetadata $metadata) + public function addMetadata($metadata) { $this->metadatas[$metadata->getClassName()] = $metadata; } public function addMetadataForValue($value, MetadataInterface $metadata) { - $this->metadatas[$value] = $metadata; + $key = is_object($value) ? spl_object_hash($value) : $value; + $this->metadatas[$key] = $metadata; } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php new file mode 100644 index 0000000000000..6a832a109f99e --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php @@ -0,0 +1,20 @@ + + * + * 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\ClassBasedInterface; +use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; + +interface LegacyClassMetadata extends MetadataInterface, PropertyMetadataContainerInterface, ClassBasedInterface +{ +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 973283c84e88c..63b8c6a551060 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\FakeClassMetadata; use Symfony\Component\Validator\Tests\Fixtures\Reference; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -386,4 +387,58 @@ public function testAddCustomizedViolation() $this->assertSame(2, $violations[0]->getMessagePluralization()); $this->assertSame('Code', $violations[0]->getCode()); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testMetadataMustImplementClassMetadataInterface() + { + $entity = new Entity(); + + $metadata = $this->getMock('Symfony\Component\Validator\Tests\Fixtures\LegacyClassMetadata'); + $metadata->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(get_class($entity))); + + $this->metadataFactory->addMetadata($metadata); + + $this->validator->validate($entity); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testReferenceMetadataMustImplementClassMetadataInterface() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $metadata = $this->getMock('Symfony\Component\Validator\Tests\Fixtures\LegacyClassMetadata'); + $metadata->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(get_class($entity->reference))); + + $this->metadataFactory->addMetadata($metadata); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->validator->validate($entity); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testPropertyMetadataMustImplementPropertyMetadataInterface() + { + $entity = new Entity(); + + // Legacy interface + $propertyMetadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + $metadata = new FakeClassMetadata(get_class($entity)); + $metadata->addPropertyMetadata('firstName', $propertyMetadata); + + $this->metadataFactory->addMetadata($metadata); + + $this->validator->validate($entity); + } } From 299c2dca10c8d4497d51f09af8a6e03574fadc9f Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 15:26:27 +0100 Subject: [PATCH 060/323] [Validator] Improved test coverage and prevented duplicate validation of constraints --- .../Constraints/CallbackValidator.php | 30 ++++--- .../Validator/Context/ExecutionContext.php | 88 +++++++++++++++++- .../Context/ExecutionContextInterface.php | 89 +++++++++++++++++++ .../Validator/NodeVisitor/AbstractVisitor.php | 15 +++- .../NodeVisitor/ContextUpdateVisitor.php | 19 ++-- ...r.php => DefaultGroupReplacingVisitor.php} | 23 ++++- .../NodeVisitor/NodeObserverInterface.php | 23 ----- .../NodeVisitor/NodeValidationVisitor.php | 39 +++++--- .../NodeVisitor/NodeVisitorInterface.php | 11 ++- .../Constraints/CallbackValidatorTest.php | 30 +++++++ .../Tests/Validator/Abstract2Dot5ApiTest.php | 57 ++++++++---- .../Validator/LegacyValidator2Dot5ApiTest.php | 4 +- .../LegacyValidatorLegacyApiTest.php | 4 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 4 +- 14 files changed, 345 insertions(+), 91 deletions(-) rename src/Symfony/Component/Validator/NodeVisitor/{GroupSequenceResolvingVisitor.php => DefaultGroupReplacingVisitor.php} (64%) delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php diff --git a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php index 39da982bb13ca..57e28fb82f137 100644 --- a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php @@ -33,10 +33,6 @@ public function validate($object, Constraint $constraint) if (!$constraint instanceof Callback) { throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Callback'); } - - if (null === $object) { - return; - } if (null !== $constraint->callback && null !== $constraint->methods) { throw new ConstraintDefinitionException( @@ -60,18 +56,24 @@ public function validate($object, Constraint $constraint) } call_user_func($method, $object, $this->context); - } else { - if (!method_exists($object, $method)) { - throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by Callback constraint does not exist', $method)); - } - $reflMethod = new \ReflectionMethod($object, $method); + continue; + } - if ($reflMethod->isStatic()) { - $reflMethod->invoke(null, $object, $this->context); - } else { - $reflMethod->invoke($object, $this->context); - } + if (null === $object) { + continue; + } + + if (!method_exists($object, $method)) { + throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by Callback constraint does not exist', $method)); + } + + $reflMethod = new \ReflectionMethod($object, $method); + + if ($reflMethod->isStatic()) { + $reflMethod->invoke(null, $object, $this->context); + } else { + $reflMethod->invoke($object, $this->context); } } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 8f7c9ee6932f3..fdfddf9c4ba9e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -19,7 +19,6 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeVisitor\NodeObserverInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -32,7 +31,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, NodeObserverInterface +class ExecutionContext implements ExecutionContextInterface { /** * @var ValidatorInterface @@ -75,6 +74,27 @@ class ExecutionContext implements ExecutionContextInterface, NodeObserverInterfa */ private $node; + /** + * Stores which objects have been validated in which group. + * + * @var array + */ + private $validatedObjects = array(); + + /** + * Stores which class constraint has been validated for which object. + * + * @var array + */ + private $validatedClassConstraints = array(); + + /** + * Stores which property constraint has been validated for which property. + * + * @var array + */ + private $validatedPropertyConstraints = array(); + /** * Creates a new execution context. * @@ -279,4 +299,68 @@ public function getMetadataFactory() 'or hasMetadataFor() instead or enable the legacy mode.' ); } + + /** + * {@inheritdoc} + */ + public function markObjectAsValidatedForGroup($objectHash, $groupHash) + { + if (!isset($this->validatedObjects[$objectHash])) { + $this->validatedObjects[$objectHash] = array(); + } + + $this->validatedObjects[$objectHash][$groupHash] = true; + } + + /** + * {@inheritdoc} + */ + public function isObjectValidatedForGroup($objectHash, $groupHash) + { + return isset($this->validatedObjects[$objectHash][$groupHash]); + } + + /** + * {@inheritdoc} + */ + public function markClassConstraintAsValidated($objectHash, $constraintHash) + { + if (!isset($this->validatedClassConstraints[$objectHash])) { + $this->validatedClassConstraints[$objectHash] = array(); + } + + $this->validatedClassConstraints[$objectHash][$constraintHash] = true; + } + + /** + * {@inheritdoc} + */ + public function isClassConstraintValidated($objectHash, $constraintHash) + { + return isset($this->validatedClassConstraints[$objectHash][$constraintHash]); + } + + /** + * {@inheritdoc} + */ + public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash) + { + if (!isset($this->validatedPropertyConstraints[$objectHash])) { + $this->validatedPropertyConstraints[$objectHash] = array(); + } + + if (!isset($this->validatedPropertyConstraints[$objectHash][$propertyName])) { + $this->validatedPropertyConstraints[$objectHash][$propertyName] = array(); + } + + $this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash] = true; + } + + /** + * {@inheritdoc} + */ + public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash) + { + return isset($this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash]); + } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index f6fed2fbee062..b955a34f27655 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; +use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -97,4 +98,92 @@ public function buildViolation($message, array $parameters = array()); * @return ValidatorInterface */ public function getValidator(); + + /** + * Sets the currently traversed node. + * + * @param Node $node The current node + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setCurrentNode(Node $node); + + /** + * Marks an object as validated in a specific validation group. + * + * @param string $objectHash The hash of the object + * @param string $groupHash The group's name or hash, if it is group + * sequence + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function markObjectAsValidatedForGroup($objectHash, $groupHash); + + /** + * Returns whether an object was validated in a specific validation group. + * + * @param string $objectHash The hash of the object + * @param string $groupHash The group's name or hash, if it is group + * sequence + * + * @return Boolean Whether the object was already validated for that + * group + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function isObjectValidatedForGroup($objectHash, $groupHash); + + /** + * Marks a constraint as validated for an object. + * + * @param string $objectHash The hash of the object + * @param string $constraintHash The hash of the constraint + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function markClassConstraintAsValidated($objectHash, $constraintHash); + + /** + * Returns whether a constraint was validated for an object. + * + * @param string $objectHash The hash of the object + * @param string $constraintHash The hash of the constraint + * + * @return Boolean Whether the constraint was already validated + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function isClassConstraintValidated($objectHash, $constraintHash); + + /** + * Marks a constraint as validated for an object and a property name. + * + * @param string $objectHash The hash of the object + * @param string $propertyName The property name + * @param string $constraintHash The hash of the constraint + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + + /** + * Returns whether a constraint was validated for an object and a property + * name. + * + * @param string $objectHash The hash of the object + * @param string $propertyName The property name + * @param string $constraintHash The hash of the constraint + * + * @return Boolean Whether the constraint was already validated + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index a47783ab0c8a7..3f15359878ed5 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -15,19 +15,32 @@ use Symfony\Component\Validator\Node\Node; /** - * @since %%NextVersion%% + * Base visitor with empty method stubs. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see NodeVisitorInterface */ abstract class AbstractVisitor implements NodeVisitorInterface { + /** + * {@inheritdoc} + */ public function beforeTraversal(array $nodes, ExecutionContextInterface $context) { } + /** + * {@inheritdoc} + */ public function afterTraversal(array $nodes, ExecutionContextInterface $context) { } + /** + * {@inheritdoc} + */ public function visit(Node $node, ExecutionContextInterface $context) { } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php index 4fb7f1b33a428..ecf0b2694c243 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php @@ -12,29 +12,24 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Node\Node; /** - * Updates the current context with the current node of the validation - * traversal. + * Informs the execution context about the currently validated node. * * @since 2.5 * @author Bernhard Schussek */ class ContextUpdateVisitor extends AbstractVisitor { + /** + * Updates the execution context. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + */ public function visit(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof NodeObserverInterface) { - throw new RuntimeException(sprintf( - 'The ContextUpdateVisitor only supports instances of class '. - '"Symfony\Component\Validator\NodeVisitor\NodeObserverInterface". '. - 'An instance of class "%s" was given.', - get_class($context) - )); - } - $context->setCurrentNode($node); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php similarity index 64% rename from src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php index 2d9b68737a1d0..6d152edf9c286 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php @@ -18,11 +18,25 @@ use Symfony\Component\Validator\Node\Node; /** - * @since %%NextVersion%% + * Checks class nodes whether their "Default" group is replaced by a group + * sequence and adjusts the validation groups accordingly. + * + * If the "Default" group is replaced for a class node, and if the validated + * groups of the node contain the group "Default", that group is replaced by + * the group sequence specified in the class' metadata. + * + * @since 2.5 * @author Bernhard Schussek */ -class GroupSequenceResolvingVisitor extends AbstractVisitor +class DefaultGroupReplacingVisitor extends AbstractVisitor { + /** + * Replaces the "Default" group in the node's groups by the class' group + * sequence. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + */ public function visit(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { @@ -30,16 +44,19 @@ public function visit(Node $node, ExecutionContextInterface $context) } if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class $groupSequence = $node->metadata->getGroupSequence(); } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ $groupSequence = $node->value->getGroupSequence(); - // TODO test if (!$groupSequence instanceof GroupSequence) { $groupSequence = new GroupSequence($groupSequence); } } else { + // The "Default" group is not overridden. Quit. return; } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php deleted file mode 100644 index 6588c09c90b90..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php +++ /dev/null @@ -1,23 +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\NodeVisitor; - -use Symfony\Component\Validator\Node\Node; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -interface NodeObserverInterface -{ - public function setCurrentNode(Node $node); -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index c688295da6ae4..e36f7880c8266 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -22,11 +22,19 @@ use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** - * @since %%NextVersion%% + * Validates a node's value against the constraints defined in it's metadata. + * + * @since 2.5 * @author Bernhard Schussek */ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface { + /** + * Stores the hashes of each validated object together with the groups + * in which that object was already validated. + * + * @var array + */ private $validatedObjects = array(); private $validatedConstraints = array(); @@ -83,19 +91,19 @@ public function visit(Node $node, ExecutionContextInterface $context) // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if (isset($this->validatedObjects[$objectHash][$groupHash])) { + if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { // Skip this group when validating properties unset($node->groups[$key]); continue; } - $this->validatedObjects[$objectHash][$groupHash] = true; + //$context->markObjectAsValidatedForGroup($objectHash, $groupHash); } // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($objectHash, $node, $group, $context); + $this->validateNodeForGroup($node, $group, $context, $objectHash); continue; } @@ -142,20 +150,31 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, } } - private function validateNodeForGroup($objectHash, Node $node, $group, ExecutionContextInterface $context) + private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { try { $this->currentGroup = $group; foreach ($node->metadata->findConstraints($group) as $constraint) { - // Remember the validated constraints of each object to prevent - // duplicate validation of constraints that belong to multiple - // validated groups + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups if (null !== $objectHash) { $constraintHash = spl_object_hash($constraint); - if (isset($this->validatedConstraints[$objectHash][$constraintHash])) { - continue; + if ($node instanceof ClassNode) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } + + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($node instanceof PropertyNode) { + $propertyName = $node->metadata->getPropertyName(); + + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; + } + + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); } $this->validatedConstraints[$objectHash][$constraintHash] = true; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index 012a64191c292..fe22a1f21969c 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -15,8 +15,17 @@ use Symfony\Component\Validator\Node\Node; /** - * @since %%NextVersion%% + * A node visitor invoked by the node traverser. + * + * At the beginning of the traversal, the method {@link beforeTraversal()} is + * called. For each traversed node, the method {@link visit()} is called. At + * last, the method {@link afterTraversal()} is called when the traversal is + * complete. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see \Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface */ interface NodeVisitorInterface { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index e0317823d52c9..98f12cb954b35 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -129,6 +129,23 @@ public function testClosure() $this->validator->validate($object, $constraint); } + public function testClosureNullObject() + { + $constraint = new Callback(function ($object, ExecutionContext $context) { + $context->addViolation('My message', array('{{ value }}' => 'foobar'), 'invalidValue'); + + return false; + }); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate(null, $constraint); + } + public function testClosureExplicitName() { $object = new CallbackValidatorTest_Object(); @@ -163,6 +180,19 @@ public function testArrayCallable() $this->validator->validate($object, $constraint); } + public function testArrayCallableNullObject() + { + $constraint = new Callback(array(__CLASS__.'_Class', 'validateCallback')); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('Callback message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate(null, $constraint); + } + public function testArrayCallableExplicitName() { $object = new CallbackValidatorTest_Object(); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 63b8c6a551060..2d34116e97fd0 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -66,25 +66,6 @@ protected function validatePropertyValue($object, $propertyName, $value, $groups return $this->validator->validatePropertyValue($object, $propertyName, $value, $groups); } - public function testNoDuplicateValidationIfConstraintInMultipleGroups() - { - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => array('Group 1', 'Group 2'), - ))); - - $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - } - public function testGroupSequenceAbortsAfterFailedGroup() { $entity = new Entity(); @@ -441,4 +422,42 @@ public function testPropertyMetadataMustImplementPropertyMetadataInterface() $this->validator->validate($entity); } + + public function testNoDuplicateValidationIfClassConstraintInMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testNoDuplicateValidationIfPropertyConstraintInMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 5468642e7d3d3..c9a8fb509d7fa 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -38,7 +38,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 3edd86b41054f..8ba44f697d2b6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -38,7 +38,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index 277d641b7aee4..fb6f6317c868a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\Validator; @@ -29,7 +29,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); From be7f055237bf877f72e1bafdd17ac4a2f9ad14b4 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:07:33 +0100 Subject: [PATCH 061/323] [Validator] Visitors may now abort the traversal by returning false from beforeTraversal() --- .../NodeTraverser/NodeTraverserInterface.php | 11 ++- .../NonRecursiveNodeTraverser.php | 84 ++++++++++++----- .../Validator/NodeVisitor/AbstractVisitor.php | 4 +- .../NodeVisitor/NodeValidationVisitor.php | 89 +++++++++++------- .../NodeVisitor/NodeVisitorInterface.php | 26 +++++- .../ObjectInitializationVisitor.php | 43 +++++++-- .../NonRecursiveNodeTraverserTest.php | 91 +++++++++++++++++++ 7 files changed, 280 insertions(+), 68 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 37c7c8c22c519..36e559ba6854e 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -25,10 +25,13 @@ * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()} * of each visitor is called. At the end of the traversal, the traverser invokes * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} - * on each visitor. + * on each visitor. The visitors are called in the same order in which they are + * added to the traverser. * - * The visitors should be called in the same order in which they are added to - * the traverser. + * If the {@link traverse()} method is called recursively, the + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} + * and {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} + * methods of the visitors will be invoked for each call. * * The validation graph typically contains nodes of the following types: * @@ -92,5 +95,5 @@ public function removeVisitor(NodeVisitorInterface $visitor); * @param Node[] $nodes The nodes to traverse * @param ExecutionContextInterface $context The validation context */ - public function traverse(array $nodes, ExecutionContextInterface $context); + public function traverse($nodes, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index b931b969431ad..811849929b31f 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -68,11 +68,6 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface */ private $metadataFactory; - /** - * @var Boolean - */ - private $traversalStarted = false; - /** * Creates a new traverser. * @@ -104,20 +99,20 @@ public function removeVisitor(NodeVisitorInterface $visitor) /** * {@inheritdoc} */ - public function traverse(array $nodes, ExecutionContextInterface $context) + public function traverse($nodes, ExecutionContextInterface $context) { - // beforeTraversal() and afterTraversal() are only executed for the - // top-level call of traverse() - $isTopLevelCall = !$this->traversalStarted; + if (!is_array($nodes)) { + $nodes = array($nodes); + } - if ($isTopLevelCall) { - // Remember that the traversal was already started for the case of - // recursive calls to traverse() - $this->traversalStarted = true; + $numberOfInitializedVisitors = $this->beforeTraversal($nodes, $context); - foreach ($this->visitors as $visitor) { - $visitor->beforeTraversal($nodes, $context); - } + // If any of the visitors requested to abort the traversal, do so, but + // clean up before + if ($numberOfInitializedVisitors < count($this->visitors)) { + $this->afterTraversal($nodes, $context, $numberOfInitializedVisitors); + + return; } // This stack contains all the nodes that should be traversed @@ -148,13 +143,60 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } - if ($isTopLevelCall) { - foreach ($this->visitors as $visitor) { - $visitor->afterTraversal($nodes, $context); + $this->afterTraversal($nodes, $context); + } + + /** + * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of + * each visitor. + * + * @param Node[] $nodes The traversed nodes + * @param ExecutionContextInterface $context The current execution context + * + * @return integer The number of successful calls. This is lower than + * the number of visitors if any of the visitors' + * beforeTraversal() methods returned false + */ + private function beforeTraversal($nodes, ExecutionContextInterface $context) + { + $numberOfCalls = 1; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->beforeTraversal($nodes, $context)) { + break; } - // Put the traverser back into its initial state - $this->traversalStarted = false; + ++$numberOfCalls; + } + + return $numberOfCalls; + } + + /** + * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of + * each visitor. + * + * @param Node[] $nodes The traversed nodes + * @param ExecutionContextInterface $context The current execution context + * @param integer|null $limit Limits the number of visitors + * on which beforeTraversal() + * should be called. All visitors + * will be called by default + */ + private function afterTraversal($nodes, ExecutionContextInterface $context, $limit = null) + { + if (null === $limit) { + $limit = count($this->visitors); + } + + $numberOfCalls = 0; + + foreach ($this->visitors as $visitor) { + $visitor->afterTraversal($nodes, $context); + + if (++$numberOfCalls === $limit) { + return; + } } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index 3f15359878ed5..c516f4a8d233b 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -27,14 +27,14 @@ abstract class AbstractVisitor implements NodeVisitorInterface /** * {@inheritdoc} */ - public function beforeTraversal(array $nodes, ExecutionContextInterface $context) + public function beforeTraversal($nodes, ExecutionContextInterface $context) { } /** * {@inheritdoc} */ - public function afterTraversal(array $nodes, ExecutionContextInterface $context) + public function afterTraversal($nodes, ExecutionContextInterface $context) { } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index e36f7880c8266..cc6f5780785ff 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -29,16 +29,6 @@ */ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface { - /** - * Stores the hashes of each validated object together with the groups - * in which that object was already validated. - * - * @var array - */ - private $validatedObjects = array(); - - private $validatedConstraints = array(); - /** * @var ConstraintValidatorFactoryInterface */ @@ -49,20 +39,37 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter */ private $nodeTraverser; + /** + * The currently validated group. + * + * @var string + */ private $currentGroup; + /** + * Creates a new visitor. + * + * @param NodeTraverserInterface $nodeTraverser The node traverser + * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory + */ public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; } - public function afterTraversal(array $nodes, ExecutionContextInterface $context) - { - $this->validatedObjects = array(); - $this->validatedConstraints = array(); - } - + /** + * Validates a node's value against the constraints defined in the node's + * metadata. + * + * Objects and constraints that were validated before in the same context + * will be skipped. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to traverse the successor nodes + */ public function visit(Node $node, ExecutionContextInterface $context) { if ($node instanceof CollectionNode) { @@ -84,21 +91,24 @@ public function visit(Node $node, ExecutionContextInterface $context) // simply continue traversal (if possible) foreach ($node->groups as $key => $group) { - // Remember which object was validated for which group - // Skip validation if the object was already validated for this - // group + // Even if we remove the following clause, the constraints on an + // object won't be validated again due to the measures taken in + // validateNodeForGroup(). + // The following shortcut, however, prevents validatedNodeForGroup() + // from being called at all and enhances performance a bit. if ($node instanceof ClassNode) { // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { - // Skip this group when validating properties + // Skip this group when validating the successor nodes + // (property and/or collection nodes) unset($node->groups[$key]); continue; } - //$context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markObjectAsValidatedForGroup($objectHash, $groupHash); } // Validate normal group @@ -108,27 +118,34 @@ public function visit(Node $node, ExecutionContextInterface $context) continue; } - // Skip the group sequence when validating properties - unset($node->groups[$key]); - // Traverse group sequence until a violation is generated $this->traverseGroupSequence($node, $group, $context); - // Optimization: If the groups only contain the group sequence, - // we can skip the traversal for the properties of the object - if (1 === count($node->groups)) { - return false; - } + // Skip the group sequence when validating successor nodes + unset($node->groups[$key]); } return true; } + /** + * {@inheritdoc} + */ public function getCurrentGroup() { return $this->currentGroup; } + /** + * Validates a node's value in each group of a group sequence. + * + * If any of the groups' constraints generates a violation, subsequent + * groups are not validated anymore. + * + * @param Node $node The validated node + * @param GroupSequence $groupSequence The group sequence + * @param ExecutionContextInterface $context The execution context + */ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); @@ -150,6 +167,17 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, } } + /** + * Validates a node's value against all constraints in the given group. + * + * @param Node $node The validated node + * @param string $group The group to validate + * @param ExecutionContextInterface $context The execution context + * @param string $objectHash The hash of the node's + * object (if any) + * + * @throws \Exception + */ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { try { @@ -176,8 +204,6 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); } - - $this->validatedConstraints[$objectHash][$constraintHash] = true; } $validator = $this->validatorFactory->getInstance($constraint); @@ -187,6 +213,7 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $this->currentGroup = null; } catch (\Exception $e) { + // Should be put into a finally block once we switch to PHP 5.5 $this->currentGroup = null; throw $e; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index fe22a1f21969c..ec05923f1bd5e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -29,9 +29,31 @@ */ interface NodeVisitorInterface { - public function beforeTraversal(array $nodes, ExecutionContextInterface $context); + /** + * Called at the beginning of a traversal. + * + * @param Node[] $nodes A list of Node instances + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to continue the traversal + */ + public function beforeTraversal($nodes, ExecutionContextInterface $context); - public function afterTraversal(array $nodes, ExecutionContextInterface $context); + /** + * Called at the end of a traversal. + * + * @param Node[] $nodes A list of Node instances + * @param ExecutionContextInterface $context The execution context + */ + public function afterTraversal($nodes, ExecutionContextInterface $context); + /** + * Called for each node during a traversal. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to traverse the node's successor nodes + */ public function visit(Node $node, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php index 05226ac6d359b..18fe19a955d71 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php @@ -12,12 +12,18 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\ObjectInitializerInterface; /** - * @since %%NextVersion%% + * Initializes the objects of all class nodes. + * + * You have to pass at least one instance of {@link ObjectInitializerInterface} + * to the constructor of this visitor. + * + * @since 2.5 * @author Bernhard Schussek */ class ObjectInitializationVisitor extends AbstractVisitor @@ -27,30 +33,51 @@ class ObjectInitializationVisitor extends AbstractVisitor */ private $initializers; + /** + * Creates a new visitor. + * + * @param ObjectInitializerInterface[] $initializers The object initializers + * + * @throws InvalidArgumentException + */ public function __construct(array $initializers) { foreach ($initializers as $initializer) { if (!$initializer instanceof ObjectInitializerInterface) { - throw new \InvalidArgumentException('Validator initializers must implement ObjectInitializerInterface.'); + throw new InvalidArgumentException(sprintf( + 'Validator initializers must implement '. + '"Symfony\Component\Validator\ObjectInitializerInterface". '. + 'Got: "%s"', + is_object($initializer) ? get_class($initializer) : gettype($initializer) + )); } } // If no initializer is present, this visitor should not even be created if (0 === count($initializers)) { - throw new \InvalidArgumentException('Please pass at least one initializer.'); + throw new InvalidArgumentException('Please pass at least one initializer.'); } $this->initializers = $initializers; } + /** + * Calls the {@link ObjectInitializerInterface::initialize()} method for + * the object of each class node. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Always returns true + */ public function visit(Node $node, ExecutionContextInterface $context) { - if (!$node instanceof ClassNode) { - return; + if ($node instanceof ClassNode) { + foreach ($this->initializers as $initializer) { + $initializer->initialize($node->value); + } } - foreach ($this->initializers as $initializer) { - $initializer->initialize($node->value); - } + return true; } } diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php new file mode 100644 index 0000000000000..4dfc7071248e9 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\NodeTraverser; + +use Symfony\Component\Validator\Mapping\GenericMetadata; +use Symfony\Component\Validator\Node\GenericNode; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class NonRecursiveNodeTraverserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FakeMetadataFactory + */ + private $metadataFactory; + + /** + * @var NonRecursiveNodeTraverser + */ + private $traverser; + + protected function setUp() + { + $this->metadataFactory = new FakeMetadataFactory(); + $this->traverser = new NonRecursiveNodeTraverser($this->metadataFactory); + } + + public function testVisitorsMayPreventTraversal() + { + $nodes = array(new GenericNode('value', new GenericMetadata(), '', array('Default'))); + $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); + + $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); + $visitor2 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); + $visitor3 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); + + $visitor1->expects($this->once()) + ->method('beforeTraversal') + ->with($nodes, $context); + + // abort traversal + $visitor2->expects($this->once()) + ->method('beforeTraversal') + ->with($nodes, $context) + ->will($this->returnValue(false)); + + // never called + $visitor3->expects($this->never()) + ->method('beforeTraversal'); + + $visitor1->expects($this->never()) + ->method('visit'); + $visitor2->expects($this->never()) + ->method('visit'); + $visitor2->expects($this->never()) + ->method('visit'); + + // called in order to clean up + $visitor1->expects($this->once()) + ->method('afterTraversal') + ->with($nodes, $context); + + // abort traversal + $visitor2->expects($this->once()) + ->method('afterTraversal') + ->with($nodes, $context); + + // never called, because beforeTraversal() wasn't called either + $visitor3->expects($this->never()) + ->method('afterTraversal'); + + $this->traverser->addVisitor($visitor1); + $this->traverser->addVisitor($visitor2); + $this->traverser->addVisitor($visitor3); + + $this->traverser->traverse($nodes, $context); + } +} From 9986f03ce25143bb358f3367ecb6a56b7d6d6743 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:10:43 +0100 Subject: [PATCH 062/323] [Validator] Added inline documentation for the PropertyPath utility class --- .../Component/Validator/Util/PropertyPath.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Util/PropertyPath.php b/src/Symfony/Component/Validator/Util/PropertyPath.php index bf33b50b5e4a6..c8f20e7200c26 100644 --- a/src/Symfony/Component/Validator/Util/PropertyPath.php +++ b/src/Symfony/Component/Validator/Util/PropertyPath.php @@ -12,11 +12,29 @@ namespace Symfony\Component\Validator\Util; /** - * @since %%NextVersion%% + * Contains utility methods for dealing with property paths. + * + * For more extensive functionality, use Symfony's PropertyAccess component. + * + * @since 2.5 * @author Bernhard Schussek */ class PropertyPath { + /** + * Appends a path to a given property path. + * + * If the base path is empty, the appended path will be returned unchanged. + * If the base path is not empty, and the appended path starts with a + * squared opening bracket ("["), the concatenation of the two paths is + * returned. Otherwise, the concatenation of the two paths is returned, + * separated by a dot ("."). + * + * @param string $basePath The base path + * @param string $subPath The path to append + * + * @return string The concatenation of the two property paths + */ public static function append($basePath, $subPath) { if ('' !== (string) $subPath) { @@ -30,6 +48,9 @@ public static function append($basePath, $subPath) return $basePath; } + /** + * Not instantiable. + */ private function __construct() { } From 524a9538bc8678a8af9625faaa92622cad1ded64 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:27:31 +0100 Subject: [PATCH 063/323] [Validator] Improved inline documentation of the validators --- .../Validator/ContextualValidator.php | 49 ++++++++-- .../ContextualValidatorInterface.php | 57 +++++++----- .../Validator/Validator/LegacyValidator.php | 5 + .../Validator/Validator/Validator.php | 23 ++++- .../Validator/ValidatorInterface.php | 91 +++++++++++++------ 5 files changed, 164 insertions(+), 61 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index cd969cc5b152e..b250278941fd8 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -24,7 +24,9 @@ use Symfony\Component\Validator\Util\PropertyPath; /** - * @since %%NextVersion%% + * Default implementation of {@link ContextualValidatorInterface}. + * + * @since 2.5 * @author Bernhard Schussek */ class ContextualValidator implements ContextualValidatorInterface @@ -44,6 +46,15 @@ class ContextualValidator implements ContextualValidatorInterface */ private $metadataFactory; + /** + * Creates a validator for the given context. + * + * @param ExecutionContextInterface $context The execution context + * @param NodeTraverserInterface $nodeTraverser The node traverser + * @param MetadataFactoryInterface $metadataFactory The factory for fetching + * the metadata of validated + * objects + */ public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) { $this->context = $context; @@ -53,13 +64,19 @@ public function __construct(ExecutionContextInterface $context, NodeTraverserInt $this->metadataFactory = $metadataFactory; } - public function atPath($subPath) + /** + * {@inheritdoc} + */ + public function atPath($path) { - $this->defaultPropertyPath = $this->context->getPropertyPath($subPath); + $this->defaultPropertyPath = $this->context->getPropertyPath($path); return $this; } + /** + * {@inheritdoc} + */ public function validate($value, $constraints = null, $groups = null) { if (null === $constraints) { @@ -84,6 +101,9 @@ public function validate($value, $constraints = null, $groups = null) return $this; } + /** + * {@inheritdoc} + */ public function validateProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -118,6 +138,9 @@ public function validateProperty($object, $propertyName, $groups = null) return $this; } + /** + * {@inheritdoc} + */ public function validatePropertyValue($object, $propertyName, $value, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -151,6 +174,21 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = return $this; } + /** + * {@inheritdoc} + */ + public function getViolations() + { + return $this->context->getViolations(); + } + + /** + * Normalizes the given group or list of groups to an array. + * + * @param mixed $groups The groups to normalize + * + * @return array A group array + */ protected function normalizeGroups($groups) { if (is_array($groups)) { @@ -159,9 +197,4 @@ protected function normalizeGroups($groups) return array($groups); } - - public function getViolations() - { - return $this->context->getViolations(); - } } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index e384228d7c977..83b5d0712098e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -15,17 +15,24 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; /** - * @since %%NextVersion%% + * A validator in a specific execution context. + * + * @since 2.5 * @author Bernhard Schussek */ interface ContextualValidatorInterface { /** - * @param string $subPath + * Appends the given path to the property path of the context. + * + * If called multiple times, the path will always be reset to the context's + * original path with the given path appended to it. + * + * @param string $path The path to append * * @return ContextualValidatorInterface This validator */ - public function atPath($subPath); + public function atPath($path); /** * Validates a value against a constraint or a list of constraints. @@ -33,46 +40,50 @@ public function atPath($subPath); * If no constraint is passed, the constraint * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. * - * @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. + * @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. If none is given, + * "Default" is assumed * * @return ContextualValidatorInterface This validator */ public function validate($value, $constraints = null, $groups = null); /** - * Validates a property of a value against its current value. + * Validates a property of an object against the constraints specified + * for this property. * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value containing the property. - * @param string $propertyName The name of the property to validate. - * @param array|null $groups The validation groups to validate. + * @param object $object The object + * @param string $propertyName The name of the validated property + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * * @return ContextualValidatorInterface This validator */ public function validateProperty($object, $propertyName, $groups = null); /** - * Validate a property of a value against a potential value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. + * Validates a value against the constraints specified for an object's + * property. * - * @param string $object The value containing the property. - * @param string $propertyName 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. + * @param object $object The object + * @param string $propertyName The name of the property + * @param mixed $value The value to validate against the + * property's constraints + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * * @return ContextualValidatorInterface This validator */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); /** - * @return ConstraintViolationListInterface + * Returns the violations that have been generated so far in the context + * of the validator. + * + * @return ConstraintViolationListInterface The constraint violations */ public function getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 9ecf522f85633..eae4746e886f4 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -17,9 +17,14 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** + * A validator that supports both the API of Symfony < 2.5 and Symfony 2.5+. + * * @since 2.5 * @author Bernhard Schussek * + * @see \Symfony\Component\Validator\ValidatorInterface + * @see \Symfony\Component\Validator\Validator\ValidatorInterface + * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. */ diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index fad080155af5f..a73a72ebb5884 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -17,7 +17,9 @@ use Symfony\Component\Validator\MetadataFactoryInterface; /** - * @since %%NextVersion%% + * Default implementation of {@link ValidatorInterface}. + * + * @since 2.5 * @author Bernhard Schussek */ class Validator implements ValidatorInterface @@ -37,6 +39,16 @@ class Validator implements ValidatorInterface */ protected $metadataFactory; + /** + * Creates a new validator. + * + * @param ExecutionContextFactoryInterface $contextFactory The factory for + * creating new contexts + * @param NodeTraverserInterface $nodeTraverser The node traverser + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + */ public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) { $this->contextFactory = $contextFactory; @@ -84,6 +96,9 @@ public function hasMetadataFor($object) return $this->metadataFactory->hasMetadataFor($object); } + /** + * {@inheritdoc} + */ public function validate($value, $constraints = null, $groups = null) { return $this->startContext($value) @@ -91,6 +106,9 @@ public function validate($value, $constraints = null, $groups = null) ->getViolations(); } + /** + * {@inheritdoc} + */ public function validateProperty($object, $propertyName, $groups = null) { return $this->startContext($object) @@ -98,6 +116,9 @@ public function validateProperty($object, $propertyName, $groups = null) ->getViolations(); } + /** + * {@inheritdoc} + */ public function validatePropertyValue($object, $propertyName, $value, $groups = null) { return $this->startContext($object) diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 124514a1ffaf7..163d1b6ca6b36 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -16,7 +16,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; /** - * @since %%NextVersion%% + * Validates PHP values against constraints. + * + * @since 2.5 * @author Bernhard Schussek */ interface ValidatorInterface @@ -27,60 +29,91 @@ interface ValidatorInterface * If no constraint is passed, the constraint * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. * - * @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. + * @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. If none is given, + * "Default" is assumed * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. + * @return ConstraintViolationListInterface A list of constraint violations. + * If the list is empty, validation + * succeeded */ public function validate($value, $constraints = null, $groups = null); /** - * Validates a property of a value against its current value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. + * Validates a property of an object against the constraints specified + * for this property. * - * @param mixed $object The value containing the property. - * @param string $propertyName The name of the property to validate. - * @param array|null $groups The validation groups to validate. + * @param object $object The object + * @param string $propertyName The name of the validated property + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. + * @return ConstraintViolationListInterface A list of constraint violations. + * If the list is empty, validation + * succeeded */ public function validateProperty($object, $propertyName, $groups = null); /** - * Validate a property of a value against a potential value. + * Validates a value against the constraints specified for an object's + * property. * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. + * @param object $object The object + * @param string $propertyName The name of the property + * @param mixed $value The value to validate against the + * property's constraints + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * - * @param string $object The value containing the property. - * @param string $propertyName 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. + * @return ConstraintViolationListInterface A list of constraint violations. + * If the list is empty, validation + * succeeded */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); /** - * @return ContextualValidatorInterface + * Starts a new validation context and returns a validator for that context. + * + * The returned validator collects all violations generated within its + * context. You can access these violations with the + * {@link ContextualValidatorInterface::getViolations()} method. + * + * @return ContextualValidatorInterface The validator for the new context */ public function startContext(); /** - * @param ExecutionContextInterface $context + * Returns a validator in the given execution context. + * + * The returned validator adds all generated violations to the given + * context. * - * @return ContextualValidatorInterface + * @param ExecutionContextInterface $context The execution context + * + * @return ContextualValidatorInterface The validator for that context */ public function inContext(ExecutionContextInterface $context); + /** + * Returns the metadata for an object. + * + * @param object $object The object + * + * @return \Symfony\Component\Validator\Mapping\MetadataInterface The metadata + * + * @throws \Symfony\Component\Validator\Exception\NoSuchMetadataException If no metadata exists + */ public function getMetadataFor($object); + /** + * Returns whether the validator has metadata for an object. + * + * @param object $object The object + * + * @return Boolean Whether metadata exists for that object + */ public function hasMetadataFor($object); } From 9ca61df9234afe72a7bf02a7e48646dfa74f6041 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:40:31 +0100 Subject: [PATCH 064/323] [Validator] Improved inline documentation of CascadingStrategy and TraversalStrategy --- .../Validator/Mapping/CascadingStrategy.php | 32 +++++++++++++++++-- .../Validator/Mapping/TraversalStrategy.php | 32 +++++++++++++++++-- .../NonRecursiveNodeTraverser.php | 2 ++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php index 1218c2d484c38..ff2853f4e0415 100644 --- a/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php @@ -12,15 +12,41 @@ namespace Symfony\Component\Validator\Mapping; /** - * @since %%NextVersion%% + * Specifies whether an object should be cascaded. + * + * Cascading is relevant for any node type but class nodes. If such a node + * contains an object of value, and if cascading is enabled, then the node + * traverser will try to find class metadata for that object and validate the + * object against that metadata. + * + * If no metadata is found for a cascaded object, and if that object implements + * {@link \Traversable}, the node traverser will iterate over the object and + * cascade each object or collection contained within, unless iteration is + * prohibited by the specified {@link TraversalStrategy}. + * + * Although the constants currently represent a boolean switch, they are + * implemented as bit mask in order to allow future extensions. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see TraversalStrategy */ class CascadingStrategy { - const NONE = 0; + /** + * Specifies that a node should not be cascaded. + */ + const NONE = 1; - const CASCADE = 1; + /** + * Specifies that a node should be cascaded. + */ + const CASCADE = 2; + /** + * Not instantiable. + */ private function __construct() { } diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 22b7f534550a2..7d74be16255f9 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -12,22 +12,50 @@ namespace Symfony\Component\Validator\Mapping; /** - * @since %%NextVersion%% + * Specifies whether and how a traversable object should be traversed. + * + * If the node traverser traverses a node whose value is an instance of + * {@link \Traversable}, and if that node is either a class node or if + * cascading is enabled, then the node's traversal strategy will be checked. + * Depending on the requested traversal strategy, the node traverser will + * iterate over the object and cascade each object or collection returned by + * the iterator. + * + * The traversal strategy is ignored for arrays. Arrays are always iterated. + * + * @since 2.1 * @author Bernhard Schussek + * + * @see CascadingStrategy */ class TraversalStrategy { /** - * @var integer + * Specifies that a node's value should be iterated only if it is an + * instance of {@link \Traversable}. */ const IMPLICIT = 1; + /** + * Specifies that a node's value should never be iterated. + */ const NONE = 2; + /** + * Specifies that a node's value should always be iterated. If the value is + * not an instance of {@link \Traversable}, an exception should be thrown. + */ const TRAVERSE = 4; + /** + * Specifies that nested instances of {@link \Traversable} should never be + * iterated. Can be combined with {@link IMPLICIT} or {@link TRAVERSE}. + */ const STOP_RECURSION = 8; + /** + * Not instantiable. + */ private function __construct() { } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index 811849929b31f..5c904f01d47f7 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -55,6 +55,8 @@ * @author Bernhard Schussek * * @see NodeTraverserInterface + * @see CascadingStrategy + * @see TraversalStrategy */ class NonRecursiveNodeTraverser implements NodeTraverserInterface { From 01ceeda376f6e6ff43e78918474a8d2e6a8c83e6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 17:10:39 +0100 Subject: [PATCH 065/323] [Validator] Improved test coverage of the Traverse constraint --- .../Validator/Constraints/Traverse.php | 2 - .../Component/Validator/Constraints/Valid.php | 10 +- .../Context/LegacyExecutionContext.php | 13 +- .../Validator/Mapping/ClassMetadata.php | 12 +- .../Validator/Mapping/GenericMetadata.php | 13 ++ .../Validator/Mapping/MemberMetadata.php | 15 -- .../Tests/Validator/Abstract2Dot5ApiTest.php | 194 +++++++++++++++++- 7 files changed, 214 insertions(+), 45 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Traverse.php b/src/Symfony/Component/Validator/Constraints/Traverse.php index 6c220561917f9..4abae6c67ab34 100644 --- a/src/Symfony/Component/Validator/Constraints/Traverse.php +++ b/src/Symfony/Component/Validator/Constraints/Traverse.php @@ -24,8 +24,6 @@ class Traverse extends Constraint { public $traverse = true; - public $deep = false; - public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 6ad09624b57d4..fe50d2d24167b 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -25,6 +25,9 @@ class Valid extends Constraint { public $traverse = true; + /** + * @deprecated Deprecated as of version 2.5, to be removed in Symfony 3.0. + */ public $deep = true; public function __construct($options = null) @@ -38,11 +41,4 @@ public function __construct($options = null) parent::__construct($options); } - - public function getDefaultOption() - { - // Traverse is extended for backwards compatibility reasons - // The parent class should be removed in 3.0 - return null; - } } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index b84d1c379c22f..1a147e441cf00 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -111,10 +111,8 @@ public function addViolationAt($subPath, $message, array $parameters = array(), public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { if (is_array($value)) { - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); + // The $traverse flag is ignored for arrays + $constraint = new Valid(array('traverse' => true, 'deep' => $deep)); return $this ->getValidator() @@ -125,16 +123,13 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals } if ($traverse && $value instanceof \Traversable) { - $constraints = array( - new Valid(), - new Traverse(array('traverse' => true, 'deep' => $deep)), - ); + $constraint = new Valid(array('traverse' => true, 'deep' => $deep)); return $this ->getValidator() ->inContext($this) ->atPath($subPath) - ->validate($value, $constraints, $groups) + ->validate($value, $constraint, $groups) ; } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 30c7b1ad3b04c..85e872026d60b 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -201,20 +201,12 @@ public function addConstraint(Constraint $constraint) } if ($constraint instanceof Traverse) { - if (true === $constraint->traverse) { + if ($constraint->traverse) { // If traverse is true, traversal should be explicitly enabled $this->traversalStrategy = TraversalStrategy::TRAVERSE; - - if (!$constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; - } - } elseif (false === $constraint->traverse) { + } else { // If traverse is false, traversal should be explicitly disabled $this->traversalStrategy = TraversalStrategy::NONE; - } else { - // Else, traverse depending on the contextual information that - // is available during validation - $this->traversalStrategy = TraversalStrategy::IMPLICIT; } // The constraint is not added diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index f77e11736a464..6c4f1869918cc 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\BadMethodCallException; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\ValidationVisitorInterface; /** @@ -103,9 +105,20 @@ public function __clone() * @param Constraint $constraint The constraint to add * * @return GenericMetadata This object + * + * @throws ConstraintDefinitionException When trying to add the + * {@link Traverse} constraint */ public function addConstraint(Constraint $constraint) { + if ($constraint instanceof Traverse) { + throw new ConstraintDefinitionException(sprintf( + 'The constraint "%s" can only be put on classes. Please use '. + '"Symfony\Component\Validator\Constraints\Valid" instead.', + get_class($constraint) + )); + } + if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 6aa0ead2ce683..60dc417024137 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -58,21 +58,6 @@ public function addConstraint(Constraint $constraint) )); } - // BC with Symfony < 2.5 - if ($constraint instanceof Valid) { - if (true === $constraint->traverse) { - // Try to traverse cascaded objects, but ignore if they do not - // implement Traversable - $this->traversalStrategy = TraversalStrategy::IMPLICIT; - - if (!$constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; - } - } elseif (false === $constraint->traverse) { - $this->traversalStrategy = TraversalStrategy::NONE; - } - } - parent::addConstraint($constraint); return $this; diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 2d34116e97fd0..4559a3ad32e01 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -328,18 +328,208 @@ public function testTraverseTraversableByDefault() $this->assertNull($violations[0]->getCode()); } + public function testTraversalEnabledOnClass() + { + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validate($traversable, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testTraversalDisabledOnClass() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(false)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validate($traversable, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + /** * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException */ - public function testExpectTraversableIfTraverseOnClass() + public function testExpectTraversableIfTraversalEnabledOnClass() { $entity = new Entity(); - $this->metadata->addConstraint(new Traverse()); + $this->metadata->addConstraint(new Traverse(true)); $this->validator->validate($entity); } + public function testReferenceTraversalDisabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(false)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testReferenceTraversalEnabledOnReferenceDisabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(false)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => true, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testReferenceTraversalDisabledOnReferenceEnabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testReferenceTraversalRecursionEnabledOnReferenceTraversalEnabledOnClass() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => true, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testReferenceTraversalRecursionDisabledOnReferenceTraversalEnabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => false, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + public function testAddCustomizedViolation() { $entity = new Entity(); From 79387a7d5e80ab3e0bbce6f35e275c778a9f9c85 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 18:00:47 +0100 Subject: [PATCH 066/323] [Validator] Improved inline documentation of the metadata classes --- .../Mapping/BlackholeMetadataFactory.php | 13 ++- .../Validator/Mapping/ClassMetadata.php | 65 ++++++++++---- .../Mapping/ClassMetadataFactory.php | 58 ++++++++++++- .../Mapping/ClassMetadataInterface.php | 52 ++++++++++- .../Validator/Mapping/ElementMetadata.php | 8 ++ .../Validator/Mapping/GenericMetadata.php | 20 ++++- .../Validator/Mapping/GetterMetadata.php | 17 ++++ .../Validator/Mapping/MemberMetadata.php | 87 +++++++++++++++---- .../Validator/Mapping/MetadataInterface.php | 47 +++++----- .../Validator/Mapping/PropertyMetadata.php | 13 +++ .../Mapping/PropertyMetadataInterface.php | 16 ++-- .../Validator/MetadataFactoryInterface.php | 12 +-- .../Component/Validator/MetadataInterface.php | 12 +-- 13 files changed, 331 insertions(+), 89 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php index 90dd282e08712..6a0f81173a976 100644 --- a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php @@ -11,25 +11,30 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\MetadataFactoryInterface; /** - * Simple implementation of MetadataFactoryInterface that can be used when using ValidatorInterface::validateValue(). + * Metadata factory that does not store metadata. + * + * This implementation is useful if you want to validate values against + * constraints only and you don't need to add constraints to classes and + * properties. * * @author Fabien Potencier */ class BlackholeMetadataFactory implements MetadataFactoryInterface { /** - * @inheritdoc + * {@inheritdoc} */ public function getMetadataFor($value) { - throw new \LogicException('BlackholeClassMetadataFactory only works with ValidatorInterface::validateValue().'); + throw new NoSuchMetadataException('This class does not support metadata.'); } /** - * @inheritdoc + * {@inheritdoc} */ public function hasMetadataFor($value) { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 85e872026d60b..e074f442922c5 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -22,7 +22,9 @@ use Symfony\Component\Validator\Exception\GroupDefinitionException; /** - * Represents all the configured constraints on a given class. + * Default implementation of {@link ClassMetadataInterface}. + * + * This class supports serialization and cloning. * * @author Bernhard Schussek * @author Fabien Potencier @@ -31,36 +33,64 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, { /** * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getClassName()} instead. */ public $name; /** * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getDefaultGroup()} instead. */ public $defaultGroup; /** * @var MemberMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadata()} instead. */ public $members = array(); /** * @var PropertyMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadata()} instead. */ public $properties = array(); /** * @var GetterMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadata()} instead. */ public $getters = array(); /** * @var array + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getGroupSequence()} instead. */ public $groupSequence = array(); /** * @var Boolean + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link isGroupSequenceProvider()} instead. */ public $groupSequenceProvider = false; @@ -70,6 +100,10 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, * By default, only instances of {@link \Traversable} are traversed. * * @var integer + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getTraversalStrategy()} instead. */ public $traversalStrategy = TraversalStrategy::IMPLICIT; @@ -94,6 +128,11 @@ public function __construct($class) } } + /** + * {@inheritdoc} + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath, $propagatedGroup = null) { if (null === $propagatedGroup && Constraint::DEFAULT_GROUP === $group @@ -129,9 +168,7 @@ public function accept(ValidationVisitorInterface $visitor, $value, $group, $pro } /** - * Returns the properties to be serialized - * - * @return array + * {@inheritdoc} */ public function __sleep() { @@ -152,9 +189,7 @@ public function __sleep() } /** - * Returns the fully qualified name of the class - * - * @return string The fully qualified class name + * {@inheritdoc} */ public function getClassName() { @@ -356,9 +391,7 @@ public function getPropertyMetadata($property) } /** - * Returns all properties for which constraints are defined. - * - * @return array An array of property names + * {@inheritdoc} */ public function getConstrainedProperties() { @@ -398,9 +431,7 @@ public function setGroupSequence($groupSequence) } /** - * Returns whether this class has an overridden default group sequence. - * - * @return Boolean + * {@inheritdoc} */ public function hasGroupSequence() { @@ -408,9 +439,7 @@ public function hasGroupSequence() } /** - * Returns the default group sequence for this class. - * - * @return GroupSequence The group sequence or null + * {@inheritdoc} */ public function getGroupSequence() { @@ -452,9 +481,7 @@ public function setGroupSequenceProvider($active) } /** - * Returns whether the class is a group sequence provider. - * - * @return Boolean + * {@inheritdoc} */ public function isGroupSequenceProvider() { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 77eb8b528f745..812ad1a8aa72d 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -17,7 +17,22 @@ use Symfony\Component\Validator\Mapping\Cache\CacheInterface; /** - * A factory for creating metadata for PHP classes. + * Creates new {@link ClassMetadataInterface} instances. + * + * Whenever {@link getMetadataFor()} is called for the first time with a given + * class name or object of that class, a new metadata instance is created and + * returned. On subsequent requests for the same class, the same metadata + * instance will be returned. + * + * You can optionally pass a {@link LoaderInterface} instance to the constructor. + * Whenever a new metadata instance, it will be passed to the loader, which can + * configure the metadata based on configuration loaded from the filesystem or + * a database. If you want to use multiple loaders, wrap them in a + * {@link Loader\LoaderChain}. + * + * You can also optionally pass a {@link CacheInterface} instance to the + * constructor. This cache will be used for persisting the generated metadata + * between multiple PHP requests. * * @author Bernhard Schussek */ @@ -25,18 +40,32 @@ class ClassMetadataFactory implements MetadataFactoryInterface { /** * The loader for loading the class metadata + * * @var LoaderInterface */ protected $loader; /** * The cache for caching class metadata + * * @var CacheInterface */ protected $cache; + /** + * The loaded metadata, indexed by class name + * + * @var ClassMetadata[] + */ protected $loadedClasses = array(); + /** + * Creates a new metadata factory. + * + * @param LoaderInterface|null $loader The loader for configuring new metadata + * @param CacheInterface|null $cache The cache for persisting metadata + * between multiple PHP requests + */ public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null) { $this->loader = $loader; @@ -44,7 +73,25 @@ public function __construct(LoaderInterface $loader = null, CacheInterface $cach } /** - * {@inheritdoc} + * Returns the metadata for the given class name or object. + * + * If the method was called with the same class name (or an object of that + * class) before, the same metadata instance is returned. + * + * If the factory was configured with a cache, this method will first look + * for an existing metadata instance in the cache. If an existing instance + * is found, it will be returned without further ado. + * + * Otherwise, a new metadata instance is created. If the factory was + * configured with a loader, the metadata is passed to the + * {@link LoaderInterface::loadClassMetadata()} method for further + * configuration. At last, the new object is returned. + * + * @param string|object $value A class name or an object + * + * @return MetadataInterface The metadata for the value + * + * @throws NoSuchMetadataException If no metadata exists for the given value */ public function getMetadataFor($value) { @@ -93,7 +140,12 @@ public function getMetadataFor($value) } /** - * {@inheritdoc} + * Returns whether the factory is able to return metadata for the given + * class name or object. + * + * @param string|object $value A class name or an object + * + * @return Boolean Whether metadata can be returned for that class */ public function hasMetadataFor($value) { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index efcfbbaa3da0c..f457cabffb02c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -15,16 +15,66 @@ use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface;; /** - * @since %%NextVersion%% + * Stores all metadata needed for validating objects of specific class. + * + * Most importantly, the metadata stores the constraints against which an object + * and its properties should be validated. + * + * Additionally, the metadata stores whether the "Default" group is overridden + * by a group sequence for that class and whether instances of that class + * should be traversed or not. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see MetadataInterface + * @see \Symfony\Component\Validator\Constraints\GroupSequence + * @see \Symfony\Component\Validator\GroupSequenceProviderInterface + * @see TraversalStrategy */ interface ClassMetadataInterface extends MetadataInterface, LegacyPropertyMetadataContainerInterface, ClassBasedInterface { + /** + * Returns the names of all constrained properties. + * + * @return string[] A list of property names + */ public function getConstrainedProperties(); + /** + * Returns whether the "Default" group is overridden by a group sequence. + * + * If it is, you can access the group sequence with {@link getGroupSequence()}. + * + * @return Boolean Returns true if the "Default" group is overridden + * + * @see \Symfony\Component\Validator\Constraints\GroupSequence + */ public function hasGroupSequence(); + /** + * Returns the group sequence that overrides the "Default" group for this + * class. + * + * @return \Symfony\Component\Validator\Constraints\GroupSequence|null The group sequence or null + * + * @see \Symfony\Component\Validator\Constraints\GroupSequence + */ public function getGroupSequence(); + /** + * Returns whether the "Default" group is overridden by a dynamic group + * sequence obtained by the validated objects. + * + * If this method returns true, the class must implement + * {@link \Symfony\Component\Validator\GroupSequenceProviderInterface}. + * This interface will be used to obtain the group sequence when an object + * of this class is validated. + * + * @return Boolean Returns true if the "Default" group is overridden by + * a dynamic group sequence + * + * @see \Symfony\Component\Validator\GroupSequenceProviderInterface + */ public function isGroupSequenceProvider(); } diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index 84a826aa10f7d..1b971c91803c1 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -11,6 +11,14 @@ namespace Symfony\Component\Validator\Mapping; +/** + * Contains the metadata of a structural element. + * + * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Extend {@link GenericMetadata} instead. + */ abstract class ElementMetadata extends GenericMetadata { } diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 6c4f1869918cc..01b3d5a403f04 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -21,6 +21,8 @@ /** * A generic container of {@link Constraint} objects. * + * This class supports serialization and cloning. + * * @since 2.5 * @author Bernhard Schussek */ @@ -28,11 +30,19 @@ class GenericMetadata implements MetadataInterface { /** * @var Constraint[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getConstraints()} and {@link findConstraints()} instead. */ public $constraints = array(); /** * @var array + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link findConstraints()} instead. */ public $constraintsByGroup = array(); @@ -44,6 +54,10 @@ class GenericMetadata implements MetadataInterface * @var integer * * @see CascadingStrategy + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getCascadingStrategy()} instead. */ public $cascadingStrategy = CascadingStrategy::NONE; @@ -55,13 +69,17 @@ class GenericMetadata implements MetadataInterface * @var integer * * @see TraversalStrategy + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getTraversalStrategy()} instead. */ public $traversalStrategy = TraversalStrategy::NONE; /** * Returns the names of the properties that should be serialized. * - * @return array + * @return string[] */ public function __sleep() { diff --git a/src/Symfony/Component/Validator/Mapping/GetterMetadata.php b/src/Symfony/Component/Validator/Mapping/GetterMetadata.php index 4bd609d035fae..122e246bdf652 100644 --- a/src/Symfony/Component/Validator/Mapping/GetterMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GetterMetadata.php @@ -13,6 +13,23 @@ use Symfony\Component\Validator\Exception\ValidatorException; +/** + * Stores all metadata needed for validating a class property via its getter + * method. + * + * A property getter is any method that is equal to the property's name, + * prefixed with either "get" or "is". That method will be used to access the + * property's value. + * + * The getter will be invoked by reflection, so the access of private and + * protected getters is supported. + * + * This class supports serialization and cloning. + * + * @author Bernhard Schussek + * + * @see PropertyMetadataInterface + */ class GetterMetadata extends MemberMetadata { /** diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 60dc417024137..fe57afa341dcc 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -16,11 +16,50 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +/** + * Stores all metadata needed for validating a class property. + * + * The method of accessing the property's value must be specified by subclasses + * by implementing the {@link newReflectionMember()} method. + * + * This class supports serialization and cloning. + * + * @author Bernhard Schussek + * + * @see PropertyMetadataInterface + */ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface { + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getClassName()} instead. + */ public $class; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getName()} instead. + */ public $name; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyName()} instead. + */ public $property; + + /** + * @var \ReflectionMethod[]|\ReflectionProperty[] + */ private $reflMember = array(); /** @@ -37,6 +76,11 @@ public function __construct($class, $name, $property) $this->property = $property; } + /** + * {@inheritdoc} + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath, $propagatedGroup = null) { $visitor->visit($this, $value, $group, $propertyPath); @@ -64,9 +108,7 @@ public function addConstraint(Constraint $constraint) } /** - * Returns the names of the properties that should be serialized - * - * @return array + * {@inheritdoc} */ public function __sleep() { @@ -78,7 +120,7 @@ public function __sleep() } /** - * Returns the name of the member + * Returns the name of the member. * * @return string */ @@ -88,9 +130,7 @@ public function getName() } /** - * Returns the class this member is defined on - * - * @return string + * {@inheritdoc} */ public function getClassName() { @@ -98,9 +138,7 @@ public function getClassName() } /** - * Returns the name of the property this member belongs to - * - * @return string The property name + * {@inheritdoc} */ public function getPropertyName() { @@ -108,7 +146,7 @@ public function getPropertyName() } /** - * Returns whether this member is public + * Returns whether this member is public. * * @param object|string $objectOrClassName The object or the class name * @@ -132,7 +170,7 @@ public function isProtected($objectOrClassName) } /** - * Returns whether this member is private + * Returns whether this member is private. * * @param object|string $objectOrClassName The object or the class name * @@ -144,9 +182,12 @@ public function isPrivate($objectOrClassName) } /** - * Returns whether objects stored in this member should be validated + * Returns whether objects stored in this member should be validated. * * @return Boolean + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link getCascadingStrategy()} instead. */ public function isCascaded() { @@ -155,9 +196,12 @@ public function isCascaded() /** * Returns whether arrays or traversable objects stored in this member - * should be traversed and validated in each entry + * should be traversed and validated in each entry. * * @return Boolean + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link getTraversalStrategy()} instead. */ public function isCollectionCascaded() { @@ -166,9 +210,12 @@ public function isCollectionCascaded() /** * Returns whether arrays or traversable objects stored in this member - * should be traversed recursively for inner arrays/traversable objects + * should be traversed recursively for inner arrays/traversable objects. * * @return Boolean + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link getTraversalStrategy()} instead. */ public function isCollectionCascadedDeeply() { @@ -176,11 +223,11 @@ public function isCollectionCascadedDeeply() } /** - * Returns the Reflection instance of the member + * Returns the reflection instance for accessing the member's value. * * @param object|string $objectOrClassName The object or the class name * - * @return object + * @return \ReflectionMethod|\ReflectionProperty The reflection instance */ public function getReflectionMember($objectOrClassName) { @@ -193,11 +240,13 @@ public function getReflectionMember($objectOrClassName) } /** - * Creates a new Reflection instance for the member + * Creates a new reflection instance for accessing the member's value. + * + * Must be implemented by subclasses. * * @param object|string $objectOrClassName The object or the class name * - * @return mixed Reflection class + * @return \ReflectionMethod|\ReflectionProperty The reflection instance */ abstract protected function newReflectionMember($objectOrClassName); } diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index c533e4c2bd344..e947c8dfe35a5 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -16,37 +16,36 @@ /** * 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}. + * Most importantly, the metadata stores the constraints against which an object + * and its properties should be validated. * - * 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. + * Additionally, the metadata stores whether objects should be validated + * against their class' metadata and whether traversable objects should be + * traversed or not. * + * @since 2.5 * @author Bernhard Schussek + * + * @see CascadingStrategy + * @see TraversalStrategy */ interface MetadataInterface extends LegacyMetadataInterface { + /** + * Returns the strategy for cascading objects. + * + * @return integer The cascading strategy + * + * @see CascadingStrategy + */ public function getCascadingStrategy(); + /** + * Returns the strategy for traversing traversable objects. + * + * @return integer The traversal strategy + * + * @see TraversalStrategy + */ public function getTraversalStrategy(); } diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php index 468f196f04a8e..f875cd68cea35 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php @@ -13,6 +13,19 @@ use Symfony\Component\Validator\Exception\ValidatorException; +/** + * Stores all metadata needed for validating a class property. + * + * The value of the property is obtained by directly accessing the property. + * The property will be accessed by reflection, so the access of private and + * protected properties is supported. + * + * This class supports serialization and cloning. + * + * @author Bernhard Schussek + * + * @see PropertyMetadataInterface + */ class PropertyMetadata extends MemberMetadata { /** diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php index 138b1c9fb59b8..79e2c799de602 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php @@ -15,17 +15,21 @@ use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; /** - * A container for validation metadata of a property. + * Stores all metadata needed for validating the value of a class 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. + * Most importantly, the metadata stores the constraints against which the + * property's value should be validated. * + * Additionally, the metadata stores whether objects stored in the property + * should be validated against their class' metadata and whether traversable + * objects should be traversed or not. + * + * @since 2.5 * @author Bernhard Schussek * * @see MetadataInterface + * @see CascadingStrategy + * @see TraversalStrategy */ interface PropertyMetadataInterface extends MetadataInterface, LegacyPropertyMetadataInterface, ClassBasedInterface { diff --git a/src/Symfony/Component/Validator/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/MetadataFactoryInterface.php index 6dbab06ab74e2..40074556c00b4 100644 --- a/src/Symfony/Component/Validator/MetadataFactoryInterface.php +++ b/src/Symfony/Component/Validator/MetadataFactoryInterface.php @@ -21,20 +21,20 @@ interface MetadataFactoryInterface /** * Returns the metadata for the given value. * - * @param mixed $value Some value. + * @param mixed $value Some value * - * @return MetadataInterface The metadata for the value. + * @return MetadataInterface The metadata for the value * - * @throws Exception\NoSuchMetadataException If no metadata exists for the value. + * @throws Exception\NoSuchMetadataException If no metadata exists for the given value */ public function getMetadataFor($value); /** - * Returns whether metadata exists for the given value. + * Returns whether the class is able to return metadata for the given value. * - * @param mixed $value Some value. + * @param mixed $value Some value * - * @return Boolean Whether metadata exists for the value. + * @return Boolean Whether metadata can be returned for that value */ public function hasMetadataFor($value); } diff --git a/src/Symfony/Component/Validator/MetadataInterface.php b/src/Symfony/Component/Validator/MetadataInterface.php index b2cb20e847b33..60abfeb42a9f0 100644 --- a/src/Symfony/Component/Validator/MetadataInterface.php +++ b/src/Symfony/Component/Validator/MetadataInterface.php @@ -53,10 +53,10 @@ interface MetadataInterface * 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. + * @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 * * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ @@ -65,9 +65,9 @@ public function accept(ValidationVisitorInterface $visitor, $value, $group, $pro /** * Returns all constraints for a given validation group. * - * @param string $group The validation group. + * @param string $group The validation group * - * @return Constraint[] A list of constraint instances. + * @return Constraint[] A list of constraint instances */ public function findConstraints($group); } From 987313d315f1083e3e5a7fd6f25b91e93cbcccaa Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 18:18:23 +0100 Subject: [PATCH 067/323] [Validator] Improved inline documentation of the violation builder --- .../Validator/ConstraintViolation.php | 59 ++++++++------ .../Validator/Context/ExecutionContext.php | 3 + .../Context/ExecutionContextFactory.php | 3 + .../Tests/Validator/Abstract2Dot5ApiTest.php | 2 +- .../Violation/ConstraintViolationBuilder.php | 77 +++++++++++++++--- .../ConstraintViolationBuilderInterface.php | 79 ++++++++++++++++++- 6 files changed, 188 insertions(+), 35 deletions(-) diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 36a42aaec4310..41f57650a13b8 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -31,12 +31,12 @@ class ConstraintViolation implements ConstraintViolationInterface /** * @var array */ - private $messageParameters; + private $parameters; /** * @var integer|null */ - private $messagePluralization; + private $plural; /** * @var mixed @@ -61,27 +61,26 @@ class ConstraintViolation implements ConstraintViolationInterface /** * Creates a new constraint violation. * - * @param string $message The violation message. - * @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($message, $messageTemplate, array $messageParameters, $root, $propertyPath, $invalidValue, $messagePluralization = null, $code = null) + * @param string $message The violation message + * @param string $messageTemplate The raw violation message + * @param array $parameters The parameters to substitute in the + * raw violation 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 that caused this + * violation + * @param integer|null $plural The number for determining the plural + * form when translation the message + * @param mixed $code The error code of the violation + */ + public function __construct($message, $messageTemplate, array $parameters, $root, $propertyPath, $invalidValue, $plural = null, $code = null) { $this->message = $message; $this->messageTemplate = $messageTemplate; - $this->messageParameters = $messageParameters; - $this->messagePluralization = $messagePluralization; + $this->parameters = $parameters; + $this->plural = $plural; $this->root = $root; $this->propertyPath = $propertyPath; $this->invalidValue = $invalidValue; @@ -130,7 +129,15 @@ public function getMessageTemplate() */ public function getMessageParameters() { - return $this->messageParameters; + return $this->parameters; + } + + /** + * Alias of {@link getMessageParameters()}. + */ + public function getParameters() + { + return $this->parameters; } /** @@ -138,7 +145,15 @@ public function getMessageParameters() */ public function getMessagePluralization() { - return $this->messagePluralization; + return $this->plural; + } + + /** + * Alias of {@link getMessagePluralization()}. + */ + public function getPlural() + { + return $this->plural; } /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index fdfddf9c4ba9e..e4db8a75b33f6 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -30,6 +30,9 @@ * @author Bernhard Schussek * * @see ExecutionContextInterface + * + * @internal You should not instantiate or use this class. Code against + * {@link ExecutionContextInterface} instead. */ class ExecutionContext implements ExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 3305e1a943452..4553c8b12633b 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -20,6 +20,9 @@ * * @since 2.5 * @author Bernhard Schussek + * + * @internal You should not instantiate or use this class. Code against + * {@link ExecutionContextFactoryInterface} instead. */ class ExecutionContextFactory implements ExecutionContextFactoryInterface { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 4559a3ad32e01..48d89e05969bc 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -538,7 +538,7 @@ public function testAddCustomizedViolation() $context->buildViolation('Message %param%') ->setParameter('%param%', 'value') ->setInvalidValue('Invalid value') - ->setPluralization(2) + ->setPlural(2) ->setCode('Code') ->addViolation(); }; diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php index 5fb8488e80fcf..d5905a0a5cbcb 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -17,29 +17,64 @@ use Symfony\Component\Validator\Util\PropertyPath; /** - * @since %%NextVersion%% + * Default implementation of {@link ConstraintViolationBuilderInterface}. + * + * @since 2.5 * @author Bernhard Schussek + * + * @internal You should not instantiate or use this class. Code against + * {@link ConstraintViolationBuilderInterface} instead. */ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface { + /** + * @var ConstraintViolationList + */ private $violations; + /** + * @var string + */ private $message; + /** + * @var array + */ private $parameters; + /** + * @var mixed + */ private $root; + /** + * @var mixed + */ private $invalidValue; + /** + * @var string + */ private $propertyPath; + /** + * @var TranslatorInterface + */ private $translator; + /** + * @var string|null + */ private $translationDomain; - private $pluralization; + /** + * @var integer|null + */ + private $plural; + /** + * @var mixed + */ private $code; public function __construct(ConstraintViolationList $violations, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = null) @@ -54,13 +89,19 @@ public function __construct(ConstraintViolationList $violations, $message, array $this->translationDomain = $translationDomain; } - public function atPath($subPath) + /** + * {@inheritdoc} + */ + public function atPath($path) { - $this->propertyPath = PropertyPath::append($this->propertyPath, $subPath); + $this->propertyPath = PropertyPath::append($this->propertyPath, $path); return $this; } + /** + * {@inheritdoc} + */ public function setParameter($key, $value) { $this->parameters[$key] = $value; @@ -68,6 +109,9 @@ public function setParameter($key, $value) return $this; } + /** + * {@inheritdoc} + */ public function setParameters(array $parameters) { $this->parameters = $parameters; @@ -75,6 +119,9 @@ public function setParameters(array $parameters) return $this; } + /** + * {@inheritdoc} + */ public function setTranslationDomain($translationDomain) { $this->translationDomain = $translationDomain; @@ -82,6 +129,9 @@ public function setTranslationDomain($translationDomain) return $this; } + /** + * {@inheritdoc} + */ public function setInvalidValue($invalidValue) { $this->invalidValue = $invalidValue; @@ -89,13 +139,19 @@ public function setInvalidValue($invalidValue) return $this; } - public function setPluralization($pluralization) + /** + * {@inheritdoc} + */ + public function setPlural($number) { - $this->pluralization = $pluralization; + $this->plural = $number; return $this; } + /** + * {@inheritdoc} + */ public function setCode($code) { $this->code = $code; @@ -103,9 +159,12 @@ public function setCode($code) return $this; } + /** + * {@inheritdoc} + */ public function addViolation() { - if (null === $this->pluralization) { + if (null === $this->plural) { $translatedMessage = $this->translator->trans( $this->message, $this->parameters, @@ -115,7 +174,7 @@ public function addViolation() try { $translatedMessage = $this->translator->transChoice( $this->message, - $this->pluralization, + $this->plural, $this->parameters, $this->translationDomain# ); @@ -135,7 +194,7 @@ public function addViolation() $this->root, $this->propertyPath, $this->invalidValue, - $this->pluralization, + $this->plural, $this->code )); } diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php index 9d62c3ccb5dce..c522860aa75e2 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php @@ -12,24 +12,97 @@ namespace Symfony\Component\Validator\Violation; /** - * @since %%NextVersion%% + * Builds {@link \Symfony\Component\Validator\ConstraintViolationInterface} + * objects. + * + * Use the various methods on this interface to configure the built violation. + * Finally, call {@link addViolation()} to add the violation to the current + * execution context. + * + * @since 2.5 * @author Bernhard Schussek */ interface ConstraintViolationBuilderInterface { - public function atPath($subPath); + /** + * Stores the property path at which the violation should be generated. + * + * The passed path will be appended to the current property path of the + * execution context. + * + * @param string $path The property path + * + * @return ConstraintViolationBuilderInterface This builder + */ + public function atPath($path); + /** + * Sets a parameter to be inserted into the violation message. + * + * @param string $key The name of the parameter + * @param string $value The value to be inserted in the parameter's place + * + * @return ConstraintViolationBuilderInterface This builder + */ public function setParameter($key, $value); + /** + * Sets all parameters to be inserted into the violation message. + * + * @param array $parameters An array with the parameter names as keys and + * the values to be inserted in their place as + * values + * + * @return ConstraintViolationBuilderInterface This builder + */ public function setParameters(array $parameters); + /** + * Sets the translation domain which should be used for translating the + * violation message. + * + * @param string $translationDomain The translation domain + * + * @return ConstraintViolationBuilderInterface This builder + * + * @see \Symfony\Component\Translation\TranslatorInterface + */ public function setTranslationDomain($translationDomain); + /** + * Sets the invalid value that caused this violation. + * + * @param mixed $invalidValue The invalid value + * + * @return ConstraintViolationBuilderInterface This builder + */ public function setInvalidValue($invalidValue); - public function setPluralization($pluralization); + /** + * Sets the number which determines how the plural form of the violation + * message is chosen when it is translated. + * + * @param integer $number The number for determining the plural form + * + * @return ConstraintViolationBuilderInterface This builder + * + * @see \Symfony\Component\Translation\TranslatorInterface::transChoice() + */ + public function setPlural($number); + /** + * Sets the violation code. + * + * @param mixed $code The violation code + * + * @return ConstraintViolationBuilderInterface This builder + * + * @internal This method is internal and should not be used by user code + */ public function setCode($code); + /** + * Adds the violation to the current execution context. + */ public function addViolation(); } From 93fdff764acf3ffdcf1c96e9b927d0dc72daf61d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 18:48:33 +0100 Subject: [PATCH 068/323] [Validator] The supported API versions can now be passed to the ValidatorBuilder --- .../Context/LegacyExecutionContext.php | 8 +- .../Mapping/BlackholeMetadataFactory.php | 3 +- .../Validator/Tests/ValidatorBuilderTest.php | 34 ++++++++ .../Component/Validator/Validation.php | 10 +++ .../Component/Validator/ValidatorBuilder.php | 75 ++++++++++++++++- .../Validator/ValidatorBuilderInterface.php | 81 ++++++++++++------- 6 files changed, 173 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 1a147e441cf00..ea238f9bcd20a 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -63,13 +63,13 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn /** * {@inheritdoc} */ - public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->setInvalidValue($invalidValue) - ->setPluralization($pluralization) + ->setPlural($plural) ->setCode($code) ->addViolation() ; @@ -83,14 +83,14 @@ public function addViolation($message, array $parameters = array(), $invalidValu /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->atPath($subPath) ->setInvalidValue($invalidValue) - ->setPluralization($pluralization) + ->setPlural($plural) ->setCode($code) ->addViolation() ; diff --git a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php index 6a0f81173a976..28eaa5f02def4 100644 --- a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\MetadataFactoryInterface; /** @@ -30,7 +29,7 @@ class BlackholeMetadataFactory implements MetadataFactoryInterface */ public function getMetadataFor($value) { - throw new NoSuchMetadataException('This class does not support metadata.'); + throw new \LogicException('This class does not support metadata.'); } /** diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index 900243f31ad65..fe45c56c07819 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests; +use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\ValidatorBuilder; use Symfony\Component\Validator\ValidatorBuilderInterface; @@ -108,4 +109,37 @@ public function testSetTranslationDomain() { $this->assertSame($this->builder, $this->builder->setTranslationDomain('TRANS_DOMAIN')); } + + public function testDefaultApiVersion() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + // Old implementation on PHP < 5.3.9 + $this->assertInstanceOf('Symfony\Component\Validator\Validator', $this->builder->getValidator()); + } else { + // Legacy compatible implementation on PHP >= 5.3.9 + $this->assertInstanceOf('Symfony\Component\Validator\Validator\LegacyValidator', $this->builder->getValidator()); + } + } + + public function testSetApiVersion24() + { + $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_4)); + $this->assertInstanceOf('Symfony\Component\Validator\Validator', $this->builder->getValidator()); + } + + public function testSetApiVersion25() + { + $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_5)); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\Validator', $this->builder->getValidator()); + } + + public function testSetApiVersion24And25() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5)); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\LegacyValidator', $this->builder->getValidator()); + } } diff --git a/src/Symfony/Component/Validator/Validation.php b/src/Symfony/Component/Validator/Validation.php index de77e838fb6ff..450a8350050e1 100644 --- a/src/Symfony/Component/Validator/Validation.php +++ b/src/Symfony/Component/Validator/Validation.php @@ -18,6 +18,16 @@ */ final class Validation { + /** + * The Validator API provided by Symfony 2.4 and older. + */ + const API_VERSION_2_4 = 1; + + /** + * The Validator API provided by Symfony 2.5 and newer. + */ + const API_VERSION_2_5 = 2; + /** * Creates a new validator. * diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index e24a7071662e2..b6763ede74e03 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -13,6 +13,9 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactory; +use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; @@ -28,6 +31,14 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; +use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; +use Symfony\Component\Validator\Validator as ValidatorV24; +use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\LegacyValidator; /** * The default implementation of {@link ValidatorBuilderInterface}. @@ -91,6 +102,11 @@ class ValidatorBuilder implements ValidatorBuilderInterface */ private $propertyAccessor; + /** + * @var integer + */ + private $apiVersion; + /** * {@inheritdoc} */ @@ -303,6 +319,32 @@ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) return $this; } + /** + * {@inheritdoc} + */ + public function setApiVersion($apiVersion) + { + if (!($apiVersion & (Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5))) { + throw new InvalidArgumentException(sprintf( + 'The requested API version is invalid: "%s"', + $apiVersion + )); + } + + if (version_compare(PHP_VERSION, '5.3.9', '<') && $apiVersion === (Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5)) { + throw new InvalidArgumentException(sprintf( + 'The Validator API that is compatible with both Symfony 2.4 '. + 'and Symfony 2.5 can only be used on PHP 5.3.9 and higher. '. + 'Your current PHP version is %s.', + PHP_VERSION + )); + } + + $this->apiVersion = $apiVersion; + + return $this; + } + /** * {@inheritdoc} */ @@ -347,7 +389,38 @@ public function getValidator() $propertyAccessor = $this->propertyAccessor ?: PropertyAccess::createPropertyAccessor(); $validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory($propertyAccessor); $translator = $this->translator ?: new DefaultTranslator(); + $apiVersion = $this->apiVersion; + + if (null === $apiVersion) { + $apiVersion = version_compare(PHP_VERSION, '5.3.9', '<') + ? Validation::API_VERSION_2_4 + : (Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5); + } + + if (Validation::API_VERSION_2_4 === $apiVersion) { + return new ValidatorV24($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); + } + + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, $validatorFactory); + + if (Validation::API_VERSION_2_5 === $apiVersion) { + $contextFactory = new ExecutionContextFactory($nodeValidator, $translator, $this->translationDomain); + } else { + $contextFactory = new LegacyExecutionContextFactory($nodeValidator, $translator, $this->translationDomain); + } + + $nodeTraverser->addVisitor(new ContextUpdateVisitor()); + if (count($this->initializers) > 0) { + $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); + } + $nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor()); + $nodeTraverser->addVisitor($nodeValidator); + + if (Validation::API_VERSION_2_5 === $apiVersion) { + return new Validator($contextFactory, $nodeTraverser, $metadataFactory); + } - return new Validator($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); + return new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); } } diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index 92aaca756a3bc..d486faed127cc 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -26,124 +26,124 @@ interface ValidatorBuilderInterface /** * Adds an object initializer to the validator. * - * @param ObjectInitializerInterface $initializer The initializer. + * @param ObjectInitializerInterface $initializer The initializer * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addObjectInitializer(ObjectInitializerInterface $initializer); /** * Adds a list of object initializers to the validator. * - * @param array $initializers The initializer. + * @param array $initializers The initializer * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addObjectInitializers(array $initializers); /** * Adds an XML constraint mapping file to the validator. * - * @param string $path The path to the mapping file. + * @param string $path The path to the mapping file * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addXmlMapping($path); /** * Adds a list of XML constraint mapping files to the validator. * - * @param array $paths The paths to the mapping files. + * @param array $paths The paths to the mapping files * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addXmlMappings(array $paths); /** * Adds a YAML constraint mapping file to the validator. * - * @param string $path The path to the mapping file. + * @param string $path The path to the mapping file * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addYamlMapping($path); /** * Adds a list of YAML constraint mappings file to the validator. * - * @param array $paths The paths to the mapping files. + * @param array $paths The paths to the mapping files * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addYamlMappings(array $paths); /** * Enables constraint mapping using the given static method. * - * @param string $methodName The name of the method. + * @param string $methodName The name of the method * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addMethodMapping($methodName); /** * Enables constraint mapping using the given static methods. * - * @param array $methodNames The names of the methods. + * @param array $methodNames The names of the methods * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addMethodMappings(array $methodNames); /** * Enables annotation based constraint mapping. * - * @param Reader $annotationReader The annotation reader to be used. + * @param Reader $annotationReader The annotation reader to be used * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function enableAnnotationMapping(Reader $annotationReader = null); /** * Disables annotation based constraint mapping. * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function disableAnnotationMapping(); /** * Sets the class metadata factory used by the validator. * - * @param MetadataFactoryInterface $metadataFactory The metadata factory. + * @param MetadataFactoryInterface $metadataFactory The metadata factory * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setMetadataFactory(MetadataFactoryInterface $metadataFactory); /** * Sets the cache for caching class metadata. * - * @param CacheInterface $cache The cache instance. + * @param CacheInterface $cache The cache instance * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setMetadataCache(CacheInterface $cache); /** * Sets the constraint validator factory used by the validator. * - * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory. + * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setConstraintValidatorFactory(ConstraintValidatorFactoryInterface $validatorFactory); /** * Sets the translator used for translating violation messages. * - * @param TranslatorInterface $translator The translator instance. + * @param TranslatorInterface $translator The translator instance * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setTranslator(TranslatorInterface $translator); @@ -154,21 +154,40 @@ public function setTranslator(TranslatorInterface $translator); * Pass the domain that is used for violation messages by default to this * method. * - * @param string $translationDomain The translation domain of the violation messages. + * @param string $translationDomain The translation domain of the violation messages * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setTranslationDomain($translationDomain); /** * Sets the property accessor for resolving property paths. * - * @param PropertyAccessorInterface $propertyAccessor The property accessor. + * @param PropertyAccessorInterface $propertyAccessor The property accessor * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor); + /** + * Sets the API versions that the returned validator should support. + * + * Use a bitwise "or" to pass multiple versions: + * + * $builder->setApiVersion(Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5); + * + * The builder will try to return an implementation that supports all + * requested versions. + * + * @param integer $apiVersion The supported API version(s) + * + * @return ValidatorBuilderInterface The builder object + * + * @see Validation::API_VERSION_2_4 + * @see Validation::API_VERSION_2_5 + */ + public function setApiVersion($apiVersion); + /** * Builds and returns a new validator object. * From 886e05e7ed0755a42bbf16f6d0f0ab1cf41958fe Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 23:48:06 +0100 Subject: [PATCH 069/323] [Validator] Removed unused use statement --- src/Symfony/Component/Validator/Mapping/MemberMetadata.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index fe57afa341dcc..9423cfd6c550c 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; From f61d31e5faa44d44f6e417c92e7b24ef10272d39 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 23:49:25 +0100 Subject: [PATCH 070/323] [Validator] Fixed grammar --- .../Component/Validator/Mapping/ClassMetadataFactory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 812ad1a8aa72d..39bc32a201e86 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -25,9 +25,9 @@ * instance will be returned. * * You can optionally pass a {@link LoaderInterface} instance to the constructor. - * Whenever a new metadata instance, it will be passed to the loader, which can - * configure the metadata based on configuration loaded from the filesystem or - * a database. If you want to use multiple loaders, wrap them in a + * Whenever a new metadata instance is created, it is passed to the loader, + * which can configure the metadata based on configuration loaded from the + * filesystem or a database. If you want to use multiple loaders, wrap them in a * {@link Loader\LoaderChain}. * * You can also optionally pass a {@link CacheInterface} instance to the From 23534ca6ab4a107a5e801e710fb3c07b1a8874d9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 22 Feb 2014 11:43:44 +0100 Subject: [PATCH 071/323] [Validator] Added a recursive clone of the new implementation for speed comparison --- .../Validator/Context/ExecutionContext.php | 89 ++- .../Context/ExecutionContextFactory.php | 12 +- .../Context/ExecutionContextInterface.php | 37 +- .../Context/LegacyExecutionContext.php | 3 +- .../Context/LegacyExecutionContextFactory.php | 12 +- .../Validator/Group/GroupManagerInterface.php | 29 - .../NodeVisitor/ContextUpdateVisitor.php | 4 +- .../NodeVisitor/NodeValidationVisitor.php | 24 +- .../Tests/Context/ExecutionContextTest.php | 69 -- .../Validator/LegacyValidator2Dot5ApiTest.php | 13 +- .../LegacyValidatorLegacyApiTest.php | 13 +- .../RecursiveValidator2Dot5ApiTest.php | 33 + ...hp => TraversingValidator2Dot5ApiTest.php} | 11 +- .../Validator/Tests/ValidatorBuilderTest.php | 2 +- src/Symfony/Component/Validator/Validator.php | 2 +- .../Validator/Validator/LegacyValidator.php | 2 +- .../RecursiveContextualValidator.php | 700 ++++++++++++++++++ .../Validator/RecursiveValidator.php | 124 ++++ ....php => TraversingContextualValidator.php} | 2 +- ...{Validator.php => TraversingValidator.php} | 6 +- .../Component/Validator/ValidatorBuilder.php | 42 +- 21 files changed, 1004 insertions(+), 225 deletions(-) delete mode 100644 src/Symfony/Component/Validator/Group/GroupManagerInterface.php delete mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php rename src/Symfony/Component/Validator/Tests/Validator/{Validator2Dot5ApiTest.php => TraversingValidator2Dot5ApiTest.php} (82%) create mode 100644 src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/RecursiveValidator.php rename src/Symfony/Component/Validator/Validator/{ContextualValidator.php => TraversingContextualValidator.php} (98%) rename src/Symfony/Component/Validator/Validator/{Validator.php => TraversingValidator.php} (95%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index e4db8a75b33f6..959f44ea79cd4 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; @@ -48,11 +49,6 @@ class ExecutionContext implements ExecutionContextInterface */ private $root; - /** - * @var GroupManagerInterface - */ - private $groupManager; - /** * @var TranslatorInterface */ @@ -71,11 +67,32 @@ class ExecutionContext implements ExecutionContextInterface private $violations; /** - * The current node under validation. + * The currently validated value. + * + * @var mixed + */ + private $value; + + /** + * The property path leading to the current value. + * + * @var string + */ + private $propertyPath = ''; + + /** + * The current validation metadata. + * + * @var MetadataInterface + */ + private $metadata; + + /** + * The currently validated group. * - * @var Node + * @var string|null */ - private $node; + private $group; /** * Stores which objects have been validated in which group. @@ -104,9 +121,6 @@ class ExecutionContext implements ExecutionContextInterface * @param ValidatorInterface $validator The validator * @param mixed $root The root value of the * validated object graph - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group * @param TranslatorInterface $translator The translator * @param string|null $translationDomain The translation domain to * use for translating @@ -115,24 +129,45 @@ class ExecutionContext implements ExecutionContextInterface * @internal Called by {@link ExecutionContextFactory}. Should not be used * in user code. */ - public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null) { $this->validator = $validator; $this->root = $root; - $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); } /** - * Sets the values of the context to match the given node. - * - * @param Node $node The currently validated node + * {@inheritdoc} + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function setMetadata(MetadataInterface $metadata = null) + { + $this->metadata = $metadata; + } + + /** + * {@inheritdoc} + */ + public function setPropertyPath($propertyPath) + { + $this->propertyPath = (string) $propertyPath; + } + + /** + * {@inheritdoc} */ - public function setCurrentNode(Node $node) + public function setGroup($group) { - $this->node = $node; + $this->group = $group; } /** @@ -209,7 +244,7 @@ public function getRoot() */ public function getValue() { - return $this->node ? $this->node->value : null; + return $this->value; } /** @@ -217,7 +252,7 @@ public function getValue() */ public function getMetadata() { - return $this->node ? $this->node->metadata : null; + return $this->metadata; } /** @@ -225,7 +260,7 @@ public function getMetadata() */ public function getGroup() { - return $this->groupManager->getCurrentGroup(); + return $this->group; } /** @@ -233,9 +268,7 @@ public function getGroup() */ public function getClassName() { - $metadata = $this->getMetadata(); - - return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null; + return $this->metadata instanceof ClassBasedInterface ? $this->metadata->getClassName() : null; } /** @@ -243,9 +276,7 @@ public function getClassName() */ public function getPropertyName() { - $metadata = $this->getMetadata(); - - return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null; + return $this->metadata instanceof PropertyMetadataInterface ? $this->metadata->getPropertyName() : null; } /** @@ -253,9 +284,7 @@ public function getPropertyName() */ public function getPropertyPath($subPath = '') { - $propertyPath = $this->node ? $this->node->propertyPath : ''; - - return PropertyPath::append($propertyPath, $subPath); + return PropertyPath::append($this->propertyPath, $subPath); } /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 4553c8b12633b..5e660f47b0c92 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -26,11 +26,6 @@ */ class ExecutionContextFactory implements ExecutionContextFactoryInterface { - /** - * @var GroupManagerInterface - */ - private $groupManager; - /** * @var TranslatorInterface */ @@ -44,17 +39,13 @@ class ExecutionContextFactory implements ExecutionContextFactoryInterface /** * Creates a new context factory. * - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group * @param TranslatorInterface $translator The translator * @param string|null $translationDomain The translation domain to * use for translating * violation messages */ - public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(TranslatorInterface $translator, $translationDomain = null) { - $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -67,7 +58,6 @@ public function createContext(ValidatorInterface $validator, $root) return new ExecutionContext( $validator, $root, - $this->groupManager, $this->translator, $this->translationDomain ); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index b955a34f27655..241dc03107865 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -100,14 +101,44 @@ public function buildViolation($message, array $parameters = array()); public function getValidator(); /** - * Sets the currently traversed node. + * Sets the currently validated value. * - * @param Node $node The current node + * @param mixed $value The validated value * * @internal Used by the validator engine. Should not be called by user * code. */ - public function setCurrentNode(Node $node); + public function setValue($value); + + /** + * Sets the current validation metadata. + * + * @param MetadataInterface $metadata The validation metadata + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setMetadata(MetadataInterface $metadata = null); + + /** + * Sets the property path leading to the current value. + * + * @param string $propertyPath The property path to the current value + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setPropertyPath($propertyPath); + + /** + * Sets the currently validated group. + * + * @param string|null $group The validated group + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setGroup($group); /** * Marks an object as validated in a specific validation group. diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index ea238f9bcd20a..abac3e9048fd8 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -42,7 +42,7 @@ class LegacyExecutionContext extends ExecutionContext * @internal Called by {@link LegacyExecutionContextFactory}. Should not be used * in user code. */ - public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null) { if (!$validator instanceof LegacyValidatorInterface) { throw new InvalidArgumentException( @@ -54,7 +54,6 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn parent::__construct( $validator, $root, - $groupManager, $translator, $translationDomain ); diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index 7f19bb204c02b..88d3c0ff5d6f8 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -26,11 +26,6 @@ */ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface { - /** - * @var GroupManagerInterface - */ - private $groupManager; - /** * @var TranslatorInterface */ @@ -44,17 +39,13 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface /** * Creates a new context factory. * - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group * @param TranslatorInterface $translator The translator * @param string|null $translationDomain The translation domain to * use for translating * violation messages */ - public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(TranslatorInterface $translator, $translationDomain = null) { - $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -67,7 +58,6 @@ public function createContext(ValidatorInterface $validator, $root) return new LegacyExecutionContext( $validator, $root, - $this->groupManager, $this->translator, $this->translationDomain ); diff --git a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php deleted file mode 100644 index a231b907d55f3..0000000000000 --- a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php +++ /dev/null @@ -1,29 +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\Group; - -/** - * Returns the group that is currently being validated. - * - * @since 2.5 - * @author Bernhard Schussek - */ -interface GroupManagerInterface -{ - /** - * Returns the group that is currently being validated. - * - * @return string|null The current group or null, if no validation is - * active. - */ - public function getCurrentGroup(); -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php index ecf0b2694c243..03243960b1d05 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php @@ -30,6 +30,8 @@ class ContextUpdateVisitor extends AbstractVisitor */ public function visit(Node $node, ExecutionContextInterface $context) { - $context->setCurrentNode($node); + $context->setValue($node->value); + $context->setMetadata($node->metadata); + $context->setPropertyPath($node->propertyPath); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index cc6f5780785ff..9d18c7ef48503 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -14,7 +14,6 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\Node; @@ -27,7 +26,7 @@ * @since 2.5 * @author Bernhard Schussek */ -class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface +class NodeValidationVisitor extends AbstractVisitor { /** * @var ConstraintValidatorFactoryInterface @@ -39,13 +38,6 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter */ private $nodeTraverser; - /** - * The currently validated group. - * - * @var string - */ - private $currentGroup; - /** * Creates a new visitor. * @@ -128,14 +120,6 @@ public function visit(Node $node, ExecutionContextInterface $context) return true; } - /** - * {@inheritdoc} - */ - public function getCurrentGroup() - { - return $this->currentGroup; - } - /** * Validates a node's value in each group of a group sequence. * @@ -181,7 +165,7 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { try { - $this->currentGroup = $group; + $context->setGroup($group); foreach ($node->metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case @@ -211,10 +195,10 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $validator->validate($node->value, $constraint); } - $this->currentGroup = null; + $context->setGroup(null); } catch (\Exception $e) { // Should be put into a finally block once we switch to PHP 5.5 - $this->currentGroup = null; + $context->setGroup(null); throw $e; } diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php deleted file mode 100644 index 0f6e51f2ffc9e..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ /dev/null @@ -1,69 +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\Context; - -use Symfony\Component\Validator\Context\ExecutionContext; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class ExecutionContextTest extends \PHPUnit_Framework_TestCase -{ - const ROOT = '__ROOT__'; - - const TRANSLATION_DOMAIN = '__TRANSLATION_DOMAIN__'; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $validator; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $groupManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $translator; - - /** - * @var ExecutionContext - */ - private $context; - - protected function setUp() - { - if (version_compare(PHP_VERSION, '5.3.9', '<')) { - $this->markTestSkipped('Not supported prior to PHP 5.3.9'); - } - - $this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface'); - $this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface'); - $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); - - $this->context = new ExecutionContext( - $this->validator, self::ROOT, $this->groupManager, $this->translator, self::TRANSLATION_DOMAIN - ); - } - - public function testGetGroup() - { - $this->groupManager->expects($this->once()) - ->method('getCurrentGroup') - ->will($this->returnValue('Current Group')); - - $this->assertSame('Current Group', $this->context->getGroup()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index c9a8fb509d7fa..3ff580b1103dd 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -34,17 +34,8 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new DefaultGroupReplacingVisitor(); - $contextRefresher = new ContextUpdateVisitor(); + $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextRefresher); - $nodeTraverser->addVisitor($nodeValidator); - - return $validator; + return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 8ba44f697d2b6..493fa7a616439 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -34,17 +34,8 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new DefaultGroupReplacingVisitor(); - $contextRefresher = new ContextUpdateVisitor(); + $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextRefresher); - $nodeTraverser->addVisitor($nodeValidator); - - return $validator; + return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php new file mode 100644 index 0000000000000..57134410dcb08 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.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\Tests\Validator; + +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Context\ExecutionContextFactory; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\Validator\RecursiveValidator; +use Symfony\Component\Validator\Validator\TraversingValidator; + +class RecursiveValidator2Dot5ApiTest extends Abstract2Dot5ApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); + + return new RecursiveValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php similarity index 82% rename from src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php rename to src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php index fb6f6317c868a..61e78456b3e00 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php @@ -19,18 +19,19 @@ use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\TraversingValidator; -class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest +class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); - $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); + $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); + $validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index fe45c56c07819..5cd1198654697 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -130,7 +130,7 @@ public function testSetApiVersion24() public function testSetApiVersion25() { $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_5)); - $this->assertInstanceOf('Symfony\Component\Validator\Validator\Validator', $this->builder->getValidator()); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\TraversingValidator', $this->builder->getValidator()); } public function testSetApiVersion24And25() diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index e80157db8123c..31dd4b9c384f5 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -22,7 +22,7 @@ * @author Bernhard Schussek * * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. - * Use {@link Validator\Validator} instead. + * Use {@link Validator\TraversingValidator} instead. */ class Validator implements ValidatorInterface { diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index eae4746e886f4..8f93b67a893e5 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -28,7 +28,7 @@ * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. */ -class LegacyValidator extends Validator implements LegacyValidatorInterface +class LegacyValidator extends RecursiveValidator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php new file mode 100644 index 0000000000000..df7901f60f4c2 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -0,0 +1,700 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; +use Symfony\Component\Validator\Exception\ValidatorException; +use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\GenericMetadata; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; +use Symfony\Component\Validator\Node\GenericNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Node\PropertyNode; +use Symfony\Component\Validator\Util\PropertyPath; + +/** + * Default implementation of {@link ContextualValidatorInterface}. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class RecursiveContextualValidator implements ContextualValidatorInterface +{ + /** + * @var ExecutionContextInterface + */ + private $context; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + private $validatorFactory; + + private $currentGroup; + + /** + * Creates a validator for the given context. + * + * @param ExecutionContextInterface $context The execution context + * @param MetadataFactoryInterface $metadataFactory The factory for fetching + * the metadata of validated + * objects + */ + public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) + { + $this->context = $context; + $this->defaultPropertyPath = $context->getPropertyPath(); + $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); + $this->metadataFactory = $metadataFactory; + $this->validatorFactory = $validatorFactory; + } + + /** + * {@inheritdoc} + */ + public function atPath($path) + { + $this->defaultPropertyPath = $this->context->getPropertyPath($path); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value, $constraints = null, $groups = null) + { + if (null === $constraints) { + $constraints = array(new Valid()); + } elseif (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->traverseGenericNode(new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ), $this->context); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validateProperty($object, $propertyName, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyValue = $propertyMetadata->getPropertyValue($object); + + $this->traverseGenericNode(new PropertyNode( + $object, + $propertyValue, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups + ), $this->context); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + foreach ($propertyMetadatas as $propertyMetadata) { + $this->traverseGenericNode(new PropertyNode( + $object, + $value, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, + $groups + ), $this->context); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getViolations() + { + return $this->context->getViolations(); + } + + /** + * Normalizes the given group or list of groups to an array. + * + * @param mixed $groups The groups to normalize + * + * @return array A group array + */ + protected function normalizeGroups($groups) + { + if (is_array($groups)) { + return $groups; + } + + return array($groups); + } + + /** + * Traverses a class node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, a property + * node is put on the node stack for each constrained property of the class. + * At last, if the class is traversable and should be traversed according + * to the selected traversal strategy, a new collection node is put on the + * stack. + * + * @param ClassNode $node The class node + * @param ExecutionContextInterface $context The current execution context + * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * + * @see ClassNode + * @see PropertyNode + * @see CollectionNode + * @see TraversalStrategy + */ + private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context) + { + if (false === $this->validateNode($node, $context)) { + return; + } + + if (0 === count($node->groups)) { + return; + } + + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + if (!$propertyMetadata instanceof PropertyMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The property metadata instances should implement '. + '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. + 'got: "%s".', + is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) + )); + } + + $this->traverseGenericNode(new PropertyNode( + $node->value, + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + ), $context); + } + } + + $traversalStrategy = $node->traversalStrategy; + + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the class' metadata + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + return; + } + + // If IMPLICIT, stop unless we deal with a Traversable + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + return; + } + + // If TRAVERSE, the constructor will fail if we have no Traversable + $this->traverseCollectionNode(new CollectionNode( + $node->value, + $node->propertyPath, + $node->groups, + $node->cascadedGroups, + $traversalStrategy + ), $context); + } + + /** + * Traverses a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - for each object in the collection with associated class metadata, a + * new class node is put on the stack; + * - if an object has no associated class metadata, but is traversable, and + * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for + * collection node, a new collection node is put on the stack for that + * object; + * - for each array in the collection, a new collection node is put on the + * stack. + * + * @param CollectionNode $node The collection node + * @param ExecutionContextInterface $context The current execution context + * + * @see ClassNode + * @see CollectionNode + */ + private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context) + { + $traversalStrategy = $node->traversalStrategy; + + if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { + $traversalStrategy = TraversalStrategy::NONE; + } else { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } + + foreach ($node->value as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->traverseCollectionNode(new CollectionNode( + $value, + $node->propertyPath.'['.$key.']', + $node->groups, + null, + $traversalStrategy + ), $context); + + continue; + } + + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->cascadeObject( + $value, + $node->propertyPath.'['.$key.']', + $node->groups, + $traversalStrategy, + $context + ); + } + } + } + + /** + * Traverses a node that is neither a class nor a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - if the node contains an object with associated class metadata, a new + * class node is put on the stack; + * - if the node contains a traversable object without associated class + * metadata and traversal is enabled according to the selected traversal + * strategy, a collection node is put on the stack; + * - if the node contains an array, a collection node is put on the stack. + * + * @param Node $node The node + * @param ExecutionContextInterface $context The current execution context + */ + private function traverseGenericNode(Node $node, ExecutionContextInterface $context) + { + if (false === $this->validateNode($node, $context)) { + return; + } + + if (null === $node->value) { + return; + } + + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->traversalStrategy; + + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the node's metadata + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->traverseCollectionNode(new CollectionNode( + $node->value, + $node->propertyPath, + $cascadedGroups, + null, + $traversalStrategy + ), $context); + + return; + } + + if ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $node->value, + $node->propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); + + return; + } + + // Currently, the traversal strategy can only be TRAVERSE for a + // generic node if the cascading strategy is CASCADE. Thus, traversable + // objects will always be handled within cascadeObject() and there's + // nothing more to do here. + + // see GenericMetadata::addConstraint() + } + + /** + * Executes the cascading logic for an object. + * + * If class metadata is available for the object, a class node is put on + * the node stack. Otherwise, if the selected traversal strategy allows + * traversal of the object, a new collection node is put on the stack. + * Otherwise, an exception is thrown. + * + * @param object $object The object to cascade + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param integer $traversalStrategy The strategy for traversing the + * cascaded object + * @param ExecutionContextInterface $context The current execution context + * + * @throws NoSuchMetadataException If the object has no associated metadata + * and does not implement {@link \Traversable} + * or if traversal is disabled via the + * $traversalStrategy argument + * @throws UnsupportedMetadataException If the metadata returned by the + * metadata factory does not implement + * {@link ClassMetadataInterface} + */ + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $this->traverseClassNode(new ClassNode( + $object, + $classMetadata, + $propertyPath, + $groups, + null, + $traversalStrategy + ), $context); + } catch (NoSuchMetadataException $e) { + // Rethrow if not Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + // Rethrow unless IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + throw $e; + } + + $this->traverseCollectionNode(new CollectionNode( + $object, + $propertyPath, + $groups, + null, + $traversalStrategy + ), $context); + } + } + + /** + * Validates a node's value against the constraints defined in the node's + * metadata. + * + * Objects and constraints that were validated before in the same context + * will be skipped. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to traverse the successor nodes + */ + public function validateNode(Node $node, ExecutionContextInterface $context) + { + if ($node instanceof CollectionNode) { + return true; + } + + $context->setValue($node->value); + $context->setMetadata($node->metadata); + $context->setPropertyPath($node->propertyPath); + + if ($node instanceof ClassNode) { + $groupSequence = null; + + if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $groupSequence = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $node->value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } + } + + if (null !== $groupSequence) { + $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); + + if (false !== $key) { + // Replace the "Default" group by the group sequence + $node->groups[$key] = $groupSequence; + + // Cascade the "Default" group when validating the sequence + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } + } + + if ($node instanceof ClassNode) { + $objectHash = spl_object_hash($node->value); + } elseif ($node instanceof PropertyNode) { + $objectHash = spl_object_hash($node->object); + } else { + $objectHash = null; + } + + // if group (=[,G3,G4]) contains group sequence (=) + // then call traverse() with each entry of the group sequence and abort + // if necessary (G1, G2) + // finally call traverse() with remaining entries ([G3,G4]) or + // simply continue traversal (if possible) + + foreach ($node->groups as $key => $group) { + // Even if we remove the following clause, the constraints on an + // object won't be validated again due to the measures taken in + // validateNodeForGroup(). + // The following shortcut, however, prevents validatedNodeForGroup() + // from being called at all and enhances performance a bit. + if ($node instanceof ClassNode) { + // Use the object hash for group sequences + $groupHash = is_object($group) ? spl_object_hash($group) : $group; + + if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + // Skip this group when validating the successor nodes + // (property and/or collection nodes) + unset($node->groups[$key]); + + continue; + } + + $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + } + + // Validate normal group + if (!$group instanceof GroupSequence) { + $this->validateNodeForGroup($node, $group, $context, $objectHash); + + continue; + } + + // Traverse group sequence until a violation is generated + $this->stepThroughGroupSequence($node, $group, $context); + + // Skip the group sequence when validating successor nodes + unset($node->groups[$key]); + } + + return true; + } + + /** + * Validates a node's value in each group of a group sequence. + * + * If any of the groups' constraints generates a violation, subsequent + * groups are not validated anymore. + * + * @param Node $node The validated node + * @param GroupSequence $groupSequence The group sequence + * @param ExecutionContextInterface $context The execution context + */ + private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + { + $violationCount = count($context->getViolations()); + + foreach ($groupSequence->groups as $groupInSequence) { + $node = clone $node; + $node->groups = array($groupInSequence); + + if (null !== $groupSequence->cascadedGroup) { + $node->cascadedGroups = array($groupSequence->cascadedGroup); + } + + if ($node instanceof ClassNode) { + $this->traverseClassNode($node, $context); + } elseif ($node instanceof CollectionNode) { + $this->traverseCollectionNode($node, $context); + } else { + $this->traverseGenericNode($node, $context); + } + + // Abort sequence validation if a violation was generated + if (count($context->getViolations()) > $violationCount) { + break; + } + } + } + + /** + * Validates a node's value against all constraints in the given group. + * + * @param Node $node The validated node + * @param string $group The group to validate + * @param ExecutionContextInterface $context The execution context + * @param string $objectHash The hash of the node's + * object (if any) + * + * @throws \Exception + */ + private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + { + try { + $context->setGroup($group); + + foreach ($node->metadata->findConstraints($group) as $constraint) { + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups + if (null !== $objectHash) { + $constraintHash = spl_object_hash($constraint); + + if ($node instanceof ClassNode) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } + + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($node instanceof PropertyNode) { + $propertyName = $node->metadata->getPropertyName(); + + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; + } + + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + } + } + + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($context); + $validator->validate($node->value, $constraint); + } + + $context->setGroup(null); + } catch (\Exception $e) { + // Should be put into a finally block once we switch to PHP 5.5 + $context->setGroup(null); + + throw $e; + } + } + + /** + * {@inheritdoc} + */ + public function getCurrentGroup() + { + return $this->currentGroup; + } +} diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php new file mode 100644 index 0000000000000..a8f9307d71e23 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; + +/** + * Default implementation of {@link ValidatorInterface}. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class RecursiveValidator implements ValidatorInterface +{ + /** + * @var ExecutionContextFactoryInterface + */ + protected $contextFactory; + + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + protected $validatorFactory; + + /** + * Creates a new validator. + * + * @param ExecutionContextFactoryInterface $contextFactory The factory for + * creating new contexts + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + */ + public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) + { + $this->contextFactory = $contextFactory; + $this->metadataFactory = $metadataFactory; + $this->validatorFactory = $validatorFactory; + } + + /** + * {@inheritdoc} + */ + public function startContext($root = null) + { + return new RecursiveContextualValidator( + $this->contextFactory->createContext($this, $root), + $this->metadataFactory, + $this->validatorFactory + ); + } + + /** + * {@inheritdoc} + */ + public function inContext(ExecutionContextInterface $context) + { + return new RecursiveContextualValidator( + $context, + $this->metadataFactory, + $this->validatorFactory + ); + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($object) + { + return $this->metadataFactory->getMetadataFor($object); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($object) + { + return $this->metadataFactory->hasMetadataFor($object); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $constraints = null, $groups = null) + { + return $this->startContext($value) + ->validate($value, $constraints, $groups) + ->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validateProperty($object, $propertyName, $groups = null) + { + return $this->startContext($object) + ->validateProperty($object, $propertyName, $groups) + ->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + return $this->startContext($object) + ->validatePropertyValue($object, $propertyName, $value, $groups) + ->getViolations(); + } +} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php similarity index 98% rename from src/Symfony/Component/Validator/Validator/ContextualValidator.php rename to src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index b250278941fd8..2f23ce71f3315 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -29,7 +29,7 @@ * @since 2.5 * @author Bernhard Schussek */ -class ContextualValidator implements ContextualValidatorInterface +class TraversingContextualValidator implements ContextualValidatorInterface { /** * @var ExecutionContextInterface diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/TraversingValidator.php similarity index 95% rename from src/Symfony/Component/Validator/Validator/Validator.php rename to src/Symfony/Component/Validator/Validator/TraversingValidator.php index a73a72ebb5884..4352b3180f81a 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingValidator.php @@ -22,7 +22,7 @@ * @since 2.5 * @author Bernhard Schussek */ -class Validator implements ValidatorInterface +class TraversingValidator implements ValidatorInterface { /** * @var ExecutionContextFactoryInterface @@ -61,7 +61,7 @@ public function __construct(ExecutionContextFactoryInterface $contextFactory, No */ public function startContext($root = null) { - return new ContextualValidator( + return new TraversingContextualValidator( $this->contextFactory->createContext($this, $root), $this->nodeTraverser, $this->metadataFactory @@ -73,7 +73,7 @@ public function startContext($root = null) */ public function inContext(ExecutionContextInterface $context) { - return new ContextualValidator( + return new TraversingContextualValidator( $context, $this->nodeTraverser, $this->metadataFactory diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index b6763ede74e03..351c5d654e73d 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator; +use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Context\ExecutionContextFactory; @@ -37,7 +38,7 @@ use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator as ValidatorV24; -use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator\LegacyValidator; /** @@ -373,6 +374,19 @@ public function getValidator() if ($this->annotationReader) { $loaders[] = new AnnotationLoader($this->annotationReader); + + AnnotationRegistry::registerLoader(function ($class) { + if (0 === strpos($class, __NAMESPACE__.'\\Constraints\\')) { + $file = str_replace(__NAMESPACE__.'\\Constraints\\', __DIR__.'/Constraints/', $class).'.php'; + + if (is_file($file)) { + require_once $file; + return true; + } + } + + return false; + }); } $loader = null; @@ -401,26 +415,24 @@ public function getValidator() return new ValidatorV24($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); } - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, $validatorFactory); - if (Validation::API_VERSION_2_5 === $apiVersion) { - $contextFactory = new ExecutionContextFactory($nodeValidator, $translator, $this->translationDomain); + $contextFactory = new ExecutionContextFactory($translator, $this->translationDomain); } else { - $contextFactory = new LegacyExecutionContextFactory($nodeValidator, $translator, $this->translationDomain); + $contextFactory = new LegacyExecutionContextFactory($translator, $this->translationDomain); } - $nodeTraverser->addVisitor(new ContextUpdateVisitor()); - if (count($this->initializers) > 0) { - $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); - } - $nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor()); - $nodeTraverser->addVisitor($nodeValidator); - if (Validation::API_VERSION_2_5 === $apiVersion) { - return new Validator($contextFactory, $nodeTraverser, $metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); + if (count($this->initializers) > 0) { + $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); + } + $nodeTraverser->addVisitor(new ContextUpdateVisitor()); + $nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor()); + $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory)); + + return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); } - return new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); + return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory); } } From 38e26fbcaf05ff547db7d1ee0c84a8afaa59b7d6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 22 Feb 2014 12:12:31 +0100 Subject: [PATCH 072/323] [Validator] Decoupled RecursiveContextualValidator from Node --- .../RecursiveContextualValidator.php | 325 ++++++++++-------- 1 file changed, 174 insertions(+), 151 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index df7901f60f4c2..e4278691af543 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -16,18 +16,19 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Util\PropertyPath; @@ -52,8 +53,6 @@ class RecursiveContextualValidator implements ContextualValidatorInterface private $validatorFactory; - private $currentGroup; - /** * Creates a validator for the given context. * @@ -96,12 +95,16 @@ public function validate($value, $constraints = null, $groups = null) $metadata->addConstraints($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $this->traverseGenericNode(new GenericNode( + $this->traverseGenericNode( $value, + null, $metadata, $this->defaultPropertyPath, - $groups - ), $this->context); + $groups, + null, + TraversalStrategy::IMPLICIT, + $this->context + ); return $this; } @@ -128,13 +131,16 @@ public function validateProperty($object, $propertyName, $groups = null) foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($object); - $this->traverseGenericNode(new PropertyNode( - $object, + $this->traverseGenericNode( $propertyValue, + $object, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups - ), $this->context); + $groups, + null, + TraversalStrategy::IMPLICIT, + $this->context + ); } return $this; @@ -160,14 +166,16 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; foreach ($propertyMetadatas as $propertyMetadata) { - $this->traverseGenericNode(new PropertyNode( - $object, + $this->traverseGenericNode( $value, + $object, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, - $groups - ), $this->context); + null, + TraversalStrategy::IMPLICIT, + $this->context + ); } return $this; @@ -218,18 +226,16 @@ protected function normalizeGroups($groups) * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context) + private function traverseClassNode($value, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - if (false === $this->validateNode($node, $context)) { - return; - } + $groups = $this->validateNode($value, $value, $metadata, $propertyPath, $groups, $traversalStrategy, $context); - if (0 === count($node->groups)) { + if (0 === count($groups)) { return; } - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + foreach ($metadata->getConstrainedProperties() as $propertyName) { + foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { if (!$propertyMetadata instanceof PropertyMetadataInterface) { throw new UnsupportedMetadataException(sprintf( 'The property metadata instances should implement '. @@ -239,26 +245,26 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c )); } - $this->traverseGenericNode(new PropertyNode( - $node->value, - $propertyMetadata->getPropertyValue($node->value), + $this->traverseGenericNode( + $propertyMetadata->getPropertyValue($value), + $value, $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName + $propertyPath + ? $propertyPath.'.'.$propertyName : $propertyName, - $node->groups, - $node->cascadedGroups - ), $context); + $groups, + $cascadedGroups, + TraversalStrategy::IMPLICIT, + $context + ); } } - $traversalStrategy = $node->traversalStrategy; - // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the class' metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { // Keep the STOP_RECURSION flag, if it was set - $traversalStrategy = $node->metadata->getTraversalStrategy() + $traversalStrategy = $metadata->getTraversalStrategy() | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } @@ -268,18 +274,28 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c } // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$value instanceof \Traversable) { return; } - // If TRAVERSE, the constructor will fail if we have no Traversable - $this->traverseCollectionNode(new CollectionNode( - $node->value, - $node->propertyPath, - $node->groups, - $node->cascadedGroups, - $traversalStrategy - ), $context); + // If TRAVERSE, fail if we have no Traversable + if (!$value instanceof \Traversable) { + // Must throw a ConstraintDefinitionException for backwards + // compatibility reasons with Symfony < 2.5 + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($value) + )); + } + + $this->cascadeCollection( + $value, + $propertyPath, + $groups, + $traversalStrategy, + $context + ); } /** @@ -304,28 +320,26 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c * @see ClassNode * @see CollectionNode */ - private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context) + private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - $traversalStrategy = $node->traversalStrategy; - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { $traversalStrategy = TraversalStrategy::NONE; } else { $traversalStrategy = TraversalStrategy::IMPLICIT; } - foreach ($node->value as $key => $value) { + foreach ($collection as $key => $value) { if (is_array($value)) { // Arrays are always cascaded, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->traverseCollectionNode(new CollectionNode( + $this->cascadeCollection( $value, - $node->propertyPath.'['.$key.']', - $node->groups, - null, - $traversalStrategy - ), $context); + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); continue; } @@ -335,8 +349,8 @@ private function traverseCollectionNode(CollectionNode $node, ExecutionContextIn if (is_object($value)) { $this->cascadeObject( $value, - $node->propertyPath.'['.$key.']', - $node->groups, + $propertyPath.'['.$key.']', + $groups, $traversalStrategy, $context ); @@ -361,48 +375,45 @@ private function traverseCollectionNode(CollectionNode $node, ExecutionContextIn * @param Node $node The node * @param ExecutionContextInterface $context The current execution context */ - private function traverseGenericNode(Node $node, ExecutionContextInterface $context) + private function traverseGenericNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - if (false === $this->validateNode($node, $context)) { + $groups = $this->validateNode($value, $object, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + + if (0 === count($groups)) { return; } - if (null === $node->value) { + if (null === $value) { return; } // The "cascadedGroups" property is set by the NodeValidationVisitor when // traversing group sequences - $cascadedGroups = null !== $node->cascadedGroups - ? $node->cascadedGroups - : $node->groups; + $cascadedGroups = count($cascadedGroups) > 0 + ? $cascadedGroups + : $groups; - if (0 === count($cascadedGroups)) { - return; - } - - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->traversalStrategy; + $cascadingStrategy = $metadata->getCascadingStrategy(); // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the node's metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { // Keep the STOP_RECURSION flag, if it was set - $traversalStrategy = $node->metadata->getTraversalStrategy() + $traversalStrategy = $metadata->getTraversalStrategy() | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } - if (is_array($node->value)) { + if (is_array($value)) { // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->traverseCollectionNode(new CollectionNode( - $node->value, - $node->propertyPath, + $this->cascadeCollection( + $value, + $propertyPath, $cascadedGroups, - null, - $traversalStrategy - ), $context); + $traversalStrategy, + $context + ); return; } @@ -412,8 +423,8 @@ private function traverseGenericNode(Node $node, ExecutionContextInterface $cont // a NoSuchMetadataException to be thrown in that case // (BC with Symfony < 2.5) $this->cascadeObject( - $node->value, - $node->propertyPath, + $value, + $propertyPath, $cascadedGroups, $traversalStrategy, $context @@ -467,14 +478,15 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal )); } - $this->traverseClassNode(new ClassNode( + $this->traverseClassNode( $object, $classMetadata, $propertyPath, $groups, null, - $traversalStrategy - ), $context); + $traversalStrategy, + $context + ); } catch (NoSuchMetadataException $e) { // Rethrow if not Traversable if (!$object instanceof \Traversable) { @@ -486,13 +498,13 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal throw $e; } - $this->traverseCollectionNode(new CollectionNode( + $this->cascadeCollection( $object, $propertyPath, $groups, - null, - $traversalStrategy - ), $context); + $traversalStrategy, + $context + ); } } @@ -506,55 +518,19 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal * @param Node $node The current node * @param ExecutionContextInterface $context The execution context * - * @return Boolean Whether to traverse the successor nodes + * @return array The groups in which the successor nodes should be validated */ - public function validateNode(Node $node, ExecutionContextInterface $context) + public function validateNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - if ($node instanceof CollectionNode) { - return true; - } - - $context->setValue($node->value); - $context->setMetadata($node->metadata); - $context->setPropertyPath($node->propertyPath); - - if ($node instanceof ClassNode) { - $groupSequence = null; - - if ($node->metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $node->metadata->getGroupSequence(); - } elseif ($node->metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $node->value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } - - if (null !== $groupSequence) { - $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); + $context->setValue($value); + $context->setMetadata($metadata); + $context->setPropertyPath($propertyPath); - if (false !== $key) { - // Replace the "Default" group by the group sequence - $node->groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } + if ($metadata instanceof ClassMetadataInterface) { + $groups = $this->replaceDefaultGroup($value, $metadata, $groups); } - if ($node instanceof ClassNode) { - $objectHash = spl_object_hash($node->value); - } elseif ($node instanceof PropertyNode) { - $objectHash = spl_object_hash($node->object); - } else { - $objectHash = null; - } + $objectHash = is_object($object) ? spl_object_hash($object) : null; // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort @@ -562,20 +538,20 @@ public function validateNode(Node $node, ExecutionContextInterface $context) // finally call traverse() with remaining entries ([G3,G4]) or // simply continue traversal (if possible) - foreach ($node->groups as $key => $group) { + foreach ($groups as $key => $group) { // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). // The following shortcut, however, prevents validatedNodeForGroup() // from being called at all and enhances performance a bit. - if ($node instanceof ClassNode) { + if ($metadata instanceof ClassMetadataInterface) { // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { // Skip this group when validating the successor nodes // (property and/or collection nodes) - unset($node->groups[$key]); + unset($groups[$key]); continue; } @@ -585,19 +561,19 @@ public function validateNode(Node $node, ExecutionContextInterface $context) // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($node, $group, $context, $objectHash); + $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); continue; } // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($node, $group, $context); + $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); // Skip the group sequence when validating successor nodes - unset($node->groups[$key]); + unset($groups[$key]); } - return true; + return $groups; } /** @@ -610,24 +586,39 @@ public function validateNode(Node $node, ExecutionContextInterface $context) * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $object, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); foreach ($groupSequence->groups as $groupInSequence) { - $node = clone $node; - $node->groups = array($groupInSequence); + $groups = array($groupInSequence); + $cascadedGroups = null; if (null !== $groupSequence->cascadedGroup) { - $node->cascadedGroups = array($groupSequence->cascadedGroup); + $cascadedGroups = array($groupSequence->cascadedGroup); } - if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $context); - } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $context); + if ($metadata instanceof ClassMetadataInterface) { + $this->traverseClassNode( + $value, + $metadata, + $propertyPath, + $groups, + $cascadedGroups, + $traversalStrategy, + $context + ); } else { - $this->traverseGenericNode($node, $context); + $this->traverseGenericNode( + $value, + $object, + $metadata, + $propertyPath, + $groups, + $cascadedGroups, + $traversalStrategy, + $context + ); } // Abort sequence validation if a violation was generated @@ -648,25 +639,25 @@ private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequen * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + private function validateNodeForGroup($value, $objectHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) { try { $context->setGroup($group); - foreach ($node->metadata->findConstraints($group) as $constraint) { + foreach ($metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups if (null !== $objectHash) { $constraintHash = spl_object_hash($constraint); - if ($node instanceof ClassNode) { + if ($metadata instanceof ClassMetadataInterface) { if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { continue; } $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($node instanceof PropertyNode) { - $propertyName = $node->metadata->getPropertyName(); + } elseif ($metadata instanceof PropertyMetadataInterface) { + $propertyName = $metadata->getPropertyName(); if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { continue; @@ -678,7 +669,7 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $validator = $this->validatorFactory->getInstance($constraint); $validator->initialize($context); - $validator->validate($node->value, $constraint); + $validator->validate($value, $constraint); } $context->setGroup(null); @@ -691,10 +682,42 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf } /** - * {@inheritdoc} + * @param $value + * @param ClassMetadataInterface $metadata + * @param array $groups + * + * @return array */ - public function getCurrentGroup() + private function replaceDefaultGroup($value, ClassMetadataInterface $metadata, array $groups) { - return $this->currentGroup; + $groupSequence = null; + + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $groupSequence = $metadata->getGroupSequence(); + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } + } + + if (null !== $groupSequence) { + $key = array_search(Constraint::DEFAULT_GROUP, $groups); + + if (false !== $key) { + // Replace the "Default" group by the group sequence + $groups[$key] = $groupSequence; + + // Cascade the "Default" group when validating the sequence + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } + + return $groups; } } From 274d4e619572cc88743cab9be37fe659de6d2ea2 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 11:00:37 +0100 Subject: [PATCH 073/323] [Validator] Changed ValidatorBuilder to always use LegacyExecutionContext This is necessary because, until Symfony 3.0, constraint validators will continue to rely on the old context methods in order to be backwards compatible. --- .../Context/LegacyExecutionContext.php | 23 ++++++++----------- .../Context/LegacyExecutionContextFactory.php | 19 +++++++++++---- .../Validator/LegacyValidator2Dot5ApiTest.php | 2 +- .../LegacyValidatorLegacyApiTest.php | 2 +- .../Validator/Validator/LegacyValidator.php | 9 ++++++++ .../Component/Validator/ValidatorBuilder.php | 6 +---- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index abac3e9048fd8..c7fb24ab2d377 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -30,33 +31,29 @@ */ class LegacyExecutionContext extends ExecutionContext { + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + /** * Creates a new context. * - * This constructor ensures that the given validator implements the - * deprecated {@link \Symfony\Component\Validator\ValidatorInterface}. If - * it does not, an {@link InvalidArgumentException} is thrown. - * * @see ExecutionContext::__construct() * * @internal Called by {@link LegacyExecutionContextFactory}. Should not be used * in user code. */ - public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, MetadataFactoryInterface $metadataFactory, TranslatorInterface $translator, $translationDomain = null) { - if (!$validator instanceof LegacyValidatorInterface) { - throw new InvalidArgumentException( - 'The validator passed to LegacyExecutionContext must implement '. - '"Symfony\Component\Validator\ValidatorInterface".' - ); - } - parent::__construct( $validator, $root, $translator, $translationDomain ); + + $this->metadataFactory = $metadataFactory; } /** @@ -158,6 +155,6 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul */ public function getMetadataFactory() { - return $this->getValidator()->getMetadataFactory(); + return $this->metadataFactory; } } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index 88d3c0ff5d6f8..b44121af7a4c1 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -13,6 +13,7 @@ use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** @@ -26,6 +27,11 @@ */ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface { + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + /** * @var TranslatorInterface */ @@ -39,13 +45,15 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface /** * Creates a new context factory. * - * @param TranslatorInterface $translator The translator - * @param string|null $translationDomain The translation domain to - * use for translating - * violation messages + * @param MetadataFactoryInterface $metadataFactory The metadata factory + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain + * to use for translating + * violation messages */ - public function __construct(TranslatorInterface $translator, $translationDomain = null) + public function __construct(MetadataFactoryInterface $metadataFactory, TranslatorInterface $translator, $translationDomain = null) { + $this->metadataFactory = $metadataFactory; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -58,6 +66,7 @@ public function createContext(ValidatorInterface $validator, $root) return new LegacyExecutionContext( $validator, $root, + $this->metadataFactory, $this->translator, $this->translationDomain ); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 3ff580b1103dd..eef4e84e7461b 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); + $contextFactory = new LegacyExecutionContextFactory($metadataFactory, new DefaultTranslator()); return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 493fa7a616439..494d9fc029e8e 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); + $contextFactory = new LegacyExecutionContextFactory($metadataFactory, new DefaultTranslator()); return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 8f93b67a893e5..12bea285c432c 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -19,6 +19,15 @@ /** * A validator that supports both the API of Symfony < 2.5 and Symfony 2.5+. * + * This class is incompatible with PHP versions < 5.3.9, because it implements + * two different interfaces specifying the same method validate(): + * + * - {@link \Symfony\Component\Validator\ValidatorInterface} + * - {@link \Symfony\Component\Validator\Validator\ValidatorInterface} + * + * In PHP versions prior to 5.3.9, either use {@link RecursiveValidator} or the + * deprecated class {@link \Symfony\Component\Validator\Validator} instead. + * * @since 2.5 * @author Bernhard Schussek * diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 351c5d654e73d..b8216f77fe215 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -415,11 +415,7 @@ public function getValidator() return new ValidatorV24($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); } - if (Validation::API_VERSION_2_5 === $apiVersion) { - $contextFactory = new ExecutionContextFactory($translator, $this->translationDomain); - } else { - $contextFactory = new LegacyExecutionContextFactory($translator, $this->translationDomain); - } + $contextFactory = new LegacyExecutionContextFactory($metadataFactory, $translator, $this->translationDomain); if (Validation::API_VERSION_2_5 === $apiVersion) { $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); From eeed509dfc43097a6243004db78e5855bdf418e7 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 11:07:57 +0100 Subject: [PATCH 074/323] [Validator] Improved phpdoc of RecursiveValidator --- .../Validator/RecursiveContextualValidator.php | 15 ++++++++++----- .../Validator/Validator/RecursiveValidator.php | 17 +++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index e4278691af543..acd79165c06c2 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -34,7 +34,7 @@ use Symfony\Component\Validator\Util\PropertyPath; /** - * Default implementation of {@link ContextualValidatorInterface}. + * Recursive implementation of {@link ContextualValidatorInterface}. * * @since 2.5 * @author Bernhard Schussek @@ -51,15 +51,20 @@ class RecursiveContextualValidator implements ContextualValidatorInterface */ private $metadataFactory; + /** + * @var ConstraintValidatorFactoryInterface + */ private $validatorFactory; /** * Creates a validator for the given context. * - * @param ExecutionContextInterface $context The execution context - * @param MetadataFactoryInterface $metadataFactory The factory for fetching - * the metadata of validated - * objects + * @param ExecutionContextInterface $context The execution context + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating + * constraint validators */ public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) { diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php index a8f9307d71e23..d0a66f3d8a740 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -17,7 +17,7 @@ use Symfony\Component\Validator\MetadataFactoryInterface; /** - * Default implementation of {@link ValidatorInterface}. + * Recursive implementation of {@link ValidatorInterface}. * * @since 2.5 * @author Bernhard Schussek @@ -34,16 +34,21 @@ class RecursiveValidator implements ValidatorInterface */ protected $metadataFactory; + /** + * @var ConstraintValidatorFactoryInterface + */ protected $validatorFactory; /** * Creates a new validator. * - * @param ExecutionContextFactoryInterface $contextFactory The factory for - * creating new contexts - * @param MetadataFactoryInterface $metadataFactory The factory for - * fetching the metadata - * of validated objects + * @param ExecutionContextFactoryInterface $contextFactory The factory for + * creating new contexts + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating + * constraint validators */ public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) { From 5c479d803c462a6455e9f3848c8229eff90801bc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 11:36:07 +0100 Subject: [PATCH 075/323] [Validator] Simplified validateNodeForGroup --- .../RecursiveContextualValidator.php | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index acd79165c06c2..ff6c6e740d52f 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -646,43 +646,34 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me */ private function validateNodeForGroup($value, $objectHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) { - try { - $context->setGroup($group); - - foreach ($metadata->findConstraints($group) as $constraint) { - // Prevent duplicate validation of constraints, in the case - // that constraints belong to multiple validated groups - if (null !== $objectHash) { - $constraintHash = spl_object_hash($constraint); + $context->setGroup($group); - if ($metadata instanceof ClassMetadataInterface) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } + foreach ($metadata->findConstraints($group) as $constraint) { + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups + if (null !== $objectHash) { + $constraintHash = spl_object_hash($constraint); - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($metadata instanceof PropertyMetadataInterface) { - $propertyName = $metadata->getPropertyName(); + if ($metadata instanceof ClassMetadataInterface) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($metadata instanceof PropertyMetadataInterface) { + $propertyName = $metadata->getPropertyName(); - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; } - } - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($context); - $validator->validate($value, $constraint); + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + } } - $context->setGroup(null); - } catch (\Exception $e) { - // Should be put into a finally block once we switch to PHP 5.5 - $context->setGroup(null); - - throw $e; + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($context); + $validator->validate($value, $constraint); } } From eed29d8ad34903f854d5323431ee59e7124cd62e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 14:16:20 +0100 Subject: [PATCH 076/323] [Validator] Improved performance of *ContextualValidator::validate() --- .../Exception/ValidatorException.php | 2 +- .../Tests/Fixtures/FakeClassMetadata.php | 3 - .../Tests/Fixtures/FakeMetadataFactory.php | 2 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 8 ++ .../Tests/Validator/AbstractValidatorTest.php | 4 + .../RecursiveContextualValidator.php | 73 ++++++++++++++----- .../TraversingContextualValidator.php | 68 +++++++++++++---- 7 files changed, 121 insertions(+), 39 deletions(-) diff --git a/src/Symfony/Component/Validator/Exception/ValidatorException.php b/src/Symfony/Component/Validator/Exception/ValidatorException.php index 6ee2416d84825..28bd4704e8fdb 100644 --- a/src/Symfony/Component/Validator/Exception/ValidatorException.php +++ b/src/Symfony/Component/Validator/Exception/ValidatorException.php @@ -11,6 +11,6 @@ namespace Symfony\Component\Validator\Exception; -class ValidatorException extends \RuntimeException +class ValidatorException extends RuntimeException { } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php index c6b79f66f336d..5ae0e68a777e8 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php @@ -11,10 +11,7 @@ namespace Symfony\Component\Validator\Tests\Fixtures; -use Symfony\Component\Validator\ClassBasedInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\MetadataInterface; -use Symfony\Component\Validator\PropertyMetadataContainerInterface; class FakeClassMetadata extends ClassMetadata { diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index 09b0ca63bea53..852eb484b0ff1 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -49,8 +49,8 @@ public function hasMetadataFor($class) $hash = null; if (is_object($class)) { + $hash = spl_object_hash($class); $class = get_class($class); - $hash = spl_object_hash($hash); } if (!is_string($class)) { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 48d89e05969bc..8698df384585b 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -650,4 +650,12 @@ public function testNoDuplicateValidationIfPropertyConstraintInMultipleGroups() /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testValidateFailsIfNoConstraintsAndNoObjectOrArray() + { + $this->validate('Foobar'); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 11e4d31eb6366..b1629ea50a470 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -837,6 +837,8 @@ public function testValidateProperty() } /** + * Cannot be UnsupportedMetadataException for BC with Symfony < 2.5. + * * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ public function testValidatePropertyFailsIfPropertiesNotSupported() @@ -903,6 +905,8 @@ public function testValidatePropertyValue() } /** + * Cannot be UnsupportedMetadataException for BC with Symfony < 2.5. + * * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ public function testValidatePropertyValueFailsIfPropertiesNotSupported() diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index ff6c6e740d52f..69b29a1c1ca73 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -13,11 +13,11 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\CascadingStrategy; @@ -90,28 +90,59 @@ public function atPath($path) */ public function validate($value, $constraints = null, $groups = null) { - if (null === $constraints) { - $constraints = array(new Valid()); - } elseif (!is_array($constraints)) { - $constraints = array($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + if (null !== $constraints) { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + + $this->traverseGenericNode( + $value, + null, + $metadata, + $this->defaultPropertyPath, + $groups, + null, + TraversalStrategy::IMPLICIT, + $this->context + ); + + return $this; } - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + if (is_object($value)) { + $this->cascadeObject( + $value, + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context + ); - $this->traverseGenericNode( - $value, - null, - $metadata, - $this->defaultPropertyPath, - $groups, - null, - TraversalStrategy::IMPLICIT, - $this->context - ); + return $this; + } - return $this; + if (is_array($value)) { + $this->cascadeCollection( + $value, + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context + ); + + return $this; + } + + throw new RuntimeException(sprintf( + 'Cannot validate values of type "%s" automatically. Please '. + 'provide a constraint.', + gettype($value) + )); } /** @@ -122,6 +153,8 @@ public function validateProperty($object, $propertyName, $groups = null) $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. @@ -159,6 +192,8 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index 2f23ce71f3315..288fd8a66c603 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -12,12 +12,15 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\RuntimeException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; @@ -79,22 +82,53 @@ public function atPath($path) */ public function validate($value, $constraints = null, $groups = null) { - if (null === $constraints) { - $constraints = array(new Valid()); - } elseif (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $node = new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups - ); + if (null !== $constraints) { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + + $node = new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ); + } elseif (is_array($value) || $value instanceof \Traversable && !$this->metadataFactory->hasMetadataFor($value)) { + $node = new CollectionNode( + $value, + $this->defaultPropertyPath, + $groups + ); + } elseif (is_object($value)) { + $metadata = $this->metadataFactory->getMetadataFor($value); + + if (!$metadata instanceof ClassMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($metadata) ? get_class($metadata) : gettype($metadata) + )); + } + + $node = new ClassNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ); + } else { + throw new RuntimeException(sprintf( + 'Cannot validate values of type "%s" automatically. Please '. + 'provide a constraint.', + gettype($value) + )); + } $this->nodeTraverser->traverse(array($node), $this->context); @@ -109,6 +143,8 @@ public function validateProperty($object, $propertyName, $groups = null) $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. @@ -146,6 +182,8 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. From 50bb84d06bc8b437000a1d21db83b6cc4b951fd2 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 14:45:21 +0100 Subject: [PATCH 077/323] [Validator] Optimized RecursiveContextualValidator --- .../NodeVisitor/NodeValidationVisitor.php | 16 ++++++------- .../RecursiveContextualValidator.php | 23 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 9d18c7ef48503..65e9bcc78868f 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -103,18 +103,18 @@ public function visit(Node $node, ExecutionContextInterface $context) $context->markObjectAsValidatedForGroup($objectHash, $groupHash); } - // Validate normal group - if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($node, $group, $context, $objectHash); + if ($group instanceof GroupSequence) { + // Traverse group sequence until a violation is generated + $this->traverseGroupSequence($node, $group, $context); + + // Skip the group sequence when validating successor nodes + unset($node->groups[$key]); continue; } - // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $context); - - // Skip the group sequence when validating successor nodes - unset($node->groups[$key]); + // Validate normal group + $this->validateNodeForGroup($node, $group, $context, $objectHash); } return true; diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 69b29a1c1ca73..e3872b4232c3f 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -268,6 +268,9 @@ protected function normalizeGroups($groups) */ private function traverseClassNode($value, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { + // Replace "Default" group by group sequence, if appropriate + $groups = $this->replaceDefaultGroup($value, $metadata, $groups); + $groups = $this->validateNode($value, $value, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { @@ -566,10 +569,6 @@ public function validateNode($value, $object, MetadataInterface $metadata = null $context->setMetadata($metadata); $context->setPropertyPath($propertyPath); - if ($metadata instanceof ClassMetadataInterface) { - $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - } - $objectHash = is_object($object) ? spl_object_hash($object) : null; // if group (=[,G3,G4]) contains group sequence (=) @@ -599,18 +598,18 @@ public function validateNode($value, $object, MetadataInterface $metadata = null $context->markObjectAsValidatedForGroup($objectHash, $groupHash); } - // Validate normal group - if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); + if ($group instanceof GroupSequence) { + // Traverse group sequence until a violation is generated + $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); continue; } - // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); - - // Skip the group sequence when validating successor nodes - unset($groups[$key]); + // Validate normal group + $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); } return $groups; From be508e01dd179a57d45001f0cbfbcdcf75aa1f49 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 14:56:15 +0100 Subject: [PATCH 078/323] [Validator] Merged DefaultGroupReplacingVisitor and ContextUpdateVisitor into NodeValidationVisitor --- .../NodeVisitor/ContextUpdateVisitor.php | 37 ---------- .../DefaultGroupReplacingVisitor.php | 73 ------------------- .../NodeVisitor/NodeValidationVisitor.php | 47 ++++++++++++ .../TraversingValidator2Dot5ApiTest.php | 8 +- 4 files changed, 48 insertions(+), 117 deletions(-) delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php deleted file mode 100644 index 03243960b1d05..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php +++ /dev/null @@ -1,37 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; - -/** - * Informs the execution context about the currently validated node. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class ContextUpdateVisitor extends AbstractVisitor -{ - /** - * Updates the execution context. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - $context->setValue($node->value); - $context->setMetadata($node->metadata); - $context->setPropertyPath($node->propertyPath); - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php deleted file mode 100644 index 6d152edf9c286..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php +++ /dev/null @@ -1,73 +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\NodeVisitor; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\Node; - -/** - * Checks class nodes whether their "Default" group is replaced by a group - * sequence and adjusts the validation groups accordingly. - * - * If the "Default" group is replaced for a class node, and if the validated - * groups of the node contain the group "Default", that group is replaced by - * the group sequence specified in the class' metadata. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class DefaultGroupReplacingVisitor extends AbstractVisitor -{ - /** - * Replaces the "Default" group in the node's groups by the class' group - * sequence. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - if (!$node instanceof ClassNode) { - return; - } - - if ($node->metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $node->metadata->getGroupSequence(); - } elseif ($node->metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $node->value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } else { - // The "Default" group is not overridden. Quit. - return; - } - - $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); - - if (false !== $key) { - // Replace the "Default" group by the group sequence - $node->groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 65e9bcc78868f..3a5a0f2bf8a33 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -68,7 +69,13 @@ public function visit(Node $node, ExecutionContextInterface $context) return true; } + $context->setValue($node->value); + $context->setMetadata($node->metadata); + $context->setPropertyPath($node->propertyPath); + if ($node instanceof ClassNode) { + $this->replaceDefaultGroup($node); + $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { $objectHash = spl_object_hash($node->object); @@ -203,4 +210,44 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf throw $e; } } + + /** + * Checks class nodes whether their "Default" group is replaced by a group + * sequence and adjusts the validation groups accordingly. + * + * If the "Default" group is replaced for a class node, and if the validated + * groups of the node contain the group "Default", that group is replaced by + * the group sequence specified in the class' metadata. + * + * @param ClassNode $node The node + */ + private function replaceDefaultGroup(ClassNode $node) + { + if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $groupSequence = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $node->value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } + } else { + // The "Default" group is not overridden. Quit. + return; + } + + $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); + + if (false !== $key) { + // Replace the "Default" group by the group sequence + $node->groups[$key] = $groupSequence; + + // Cascade the "Default" group when validating the sequence + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php index 61e78456b3e00..7f96944b77ce3 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php @@ -29,13 +29,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); $validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new DefaultGroupReplacingVisitor(); - $contextRefresher = new ContextUpdateVisitor(); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextRefresher); - $nodeTraverser->addVisitor($nodeValidator); + $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory())); return $validator; } From 1622eb3a98a9f74a1e6b3eabfcb35176547461a6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 15:16:49 +0100 Subject: [PATCH 079/323] [Validator] Fixed reference to removed class in ValidatorBuilder --- src/Symfony/Component/Validator/ValidatorBuilder.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index b8216f77fe215..4f536c15551c4 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -14,7 +14,6 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; @@ -33,8 +32,6 @@ use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Cache\ArrayCache; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator as ValidatorV24; @@ -422,8 +419,6 @@ public function getValidator() if (count($this->initializers) > 0) { $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); } - $nodeTraverser->addVisitor(new ContextUpdateVisitor()); - $nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor()); $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory)); return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); From 94ef21e495155836e41dee515550121e160710fb Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 16:44:19 +0100 Subject: [PATCH 080/323] [Validator] Optimized use statements --- .../Component/Validator/Constraint.php | 2 +- .../Constraints/CallbackValidator.php | 2 +- .../Constraints/ExpressionValidator.php | 2 +- .../Validator/Constraints/FileValidator.php | 4 +-- .../Component/Validator/Constraints/Ip.php | 2 +- .../Validator/Context/ExecutionContext.php | 2 -- .../Context/ExecutionContextFactory.php | 1 - .../Context/ExecutionContextInterface.php | 1 - .../Context/LegacyExecutionContext.php | 4 --- .../Context/LegacyExecutionContextFactory.php | 1 - .../Component/Validator/DefaultTranslator.php | 2 +- .../Validator/Mapping/Cache/DoctrineCache.php | 2 +- .../Validator/Mapping/ClassMetadata.php | 8 +++--- .../Mapping/ClassMetadataFactory.php | 4 +-- .../Mapping/ClassMetadataInterface.php | 4 ++- .../Mapping/Loader/AbstractLoader.php | 2 +- .../Mapping/Loader/AnnotationLoader.php | 6 ++--- .../Mapping/Loader/XmlFileLoader.php | 2 +- .../Validator/Mapping/MemberMetadata.php | 2 +- .../Tests/Constraints/AllValidatorTest.php | 5 ++-- .../Constraints/CallbackValidatorTest.php | 2 +- .../Tests/Constraints/CollectionTest.php | 4 +-- .../Constraints/CollectionValidatorTest.php | 8 +++--- .../Tests/Constraints/FileValidatorTest.php | 2 +- .../Validator/Tests/ExecutionContextTest.php | 4 +-- .../Tests/Fixtures/FakeMetadataFactory.php | 1 - .../Tests/Mapping/Cache/DoctrineCacheTest.php | 2 +- .../Mapping/ClassMetadataFactoryTest.php | 5 ++-- .../Tests/Mapping/ClassMetadataTest.php | 2 -- .../Tests/Mapping/ElementMetadataTest.php | 2 +- .../Mapping/Loader/AnnotationLoaderTest.php | 2 +- .../Tests/Mapping/Loader/FilesLoaderTest.php | 2 +- .../Mapping/Loader/XmlFileLoaderTest.php | 2 +- .../Mapping/Loader/YamlFileLoaderTest.php | 2 +- .../Tests/Mapping/MemberMetadataTest.php | 6 ++--- .../Tests/Validator/AbstractValidatorTest.php | 6 ++--- .../Validator/LegacyValidator2Dot5ApiTest.php | 6 +---- .../LegacyValidatorLegacyApiTest.php | 6 +---- .../RecursiveValidator2Dot5ApiTest.php | 7 +----- .../TraversingValidator2Dot5ApiTest.php | 6 ++--- .../Validator/Tests/ValidatorTest.php | 4 +-- .../Component/Validator/ValidationVisitor.php | 2 +- src/Symfony/Component/Validator/Validator.php | 2 +- .../Validator/TraversingValidator.php | 2 +- .../Component/Validator/ValidatorBuilder.php | 25 ++++++++++--------- .../Validator/ValidatorBuilderInterface.php | 4 +-- 46 files changed, 74 insertions(+), 100 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index 0f2c226ae9f77..7f7f6b83c4208 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\InvalidOptionsException; use Symfony\Component\Validator\Exception\MissingOptionsException; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Contains the properties of a constraint definition. diff --git a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php index 57e28fb82f137..c378e55e769f7 100644 --- a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php @@ -13,8 +13,8 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validator for Callback constraint diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php index 8323e5f6dbad6..0151dfe6bc2db 100644 --- a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 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; use Symfony\Component\Validator\Exception\UnexpectedTypeException; diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php index 54f0b45d60dae..f789fb108418a 100644 --- a/src/Symfony/Component/Validator/Constraints/FileValidator.php +++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\HttpFoundation\File\File as FileObject; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\HttpFoundation\File\File as FileObject; -use Symfony\Component\HttpFoundation\File\UploadedFile; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Validator/Constraints/Ip.php b/src/Symfony/Component/Validator/Constraints/Ip.php index 099f2aabd7348..0f124f9c05bff 100644 --- a/src/Symfony/Component/Validator/Constraints/Ip.php +++ b/src/Symfony/Component/Validator/Constraints/Ip.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Constraints; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Validates that a value is a valid IP address diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 959f44ea79cd4..683ddd8c97329 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -16,10 +16,8 @@ use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 5e660f47b0c92..52bd1e6907cea 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 241dc03107865..b3224d5dd7a98 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; -use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index c7fb24ab2d377..de34b1fc2cae6 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -12,13 +12,9 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** * An execution context that is compatible with the legacy API (< 2.5). diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index b44121af7a4c1..cf5cd07e9ef54 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; diff --git a/src/Symfony/Component/Validator/DefaultTranslator.php b/src/Symfony/Component/Validator/DefaultTranslator.php index 20b2e11350839..3340cce6330b9 100644 --- a/src/Symfony/Component/Validator/DefaultTranslator.php +++ b/src/Symfony/Component/Validator/DefaultTranslator.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Translation\TranslatorInterface; /** * Simple translator implementation that simply replaces the parameters in diff --git a/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php b/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php index 56ead5d0ccc05..6dd5447fedc88 100644 --- a/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php +++ b/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Mapping\Cache; -use Symfony\Component\Validator\Mapping\ClassMetadata; use Doctrine\Common\Cache\Cache; +use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Adapts a Doctrine cache to a CacheInterface. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index e074f442922c5..28bf5cda678bf 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -11,15 +11,15 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\ValidationVisitorInterface; -use Symfony\Component\Validator\PropertyMetadataContainerInterface; -use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\GroupDefinitionException; +use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; +use Symfony\Component\Validator\ValidationVisitorInterface; /** * Default implementation of {@link ClassMetadataInterface}. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 39bc32a201e86..8c26b7acecaba 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -11,10 +11,10 @@ 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; +use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; /** * Creates new {@link ClassMetadataInterface} instances. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index f457cabffb02c..0e0d2448d7c59 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface;; +use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface; + +; /** * Stores all metadata needed for validating objects of specific class. diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php index 9b5093e1bd928..24591d6be5487 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Mapping\Loader; -use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\MappingException; abstract class AbstractLoader implements LoaderInterface { diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php index 3a624072e0242..9cd86a1a66002 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php @@ -12,12 +12,12 @@ namespace Symfony\Component\Validator\Mapping\Loader; use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Exception\MappingException; -use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequenceProvider; -use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\MappingException; +use Symfony\Component\Validator\Mapping\ClassMetadata; class AnnotationLoader implements LoaderInterface { diff --git a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php index 3f157c924195d..0cf33e0edff65 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Mapping\Loader; +use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Config\Util\XmlUtils; class XmlFileLoader extends FileLoader { diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 9423cfd6c550c..9b4860cfaf859 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\ValidationVisitorInterface; /** * Stores all metadata needed for validating a class property. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php index eaa9044e9ef52..3a654a3b457eb 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Validator\ExecutionContext; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\AllValidator; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; class AllValidatorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index 98f12cb954b35..06883e3525b15 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\ExecutionContext; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\CallbackValidator; +use Symfony\Component\Validator\ExecutionContext; class CallbackValidatorTest_Class { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php index da868d3cfdd1f..4b485a9b108b7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraints\Collection; -use Symfony\Component\Validator\Constraints\Required; -use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\Optional; +use Symfony\Component\Validator\Constraints\Required; use Symfony\Component\Validator\Constraints\Valid; /** diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php index 4a13234b69550..4dc5f7e8bc7a5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\NotNull; -use Symfony\Component\Validator\Constraints\Required; -use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\CollectionValidator; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Optional; +use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\Constraints\Required; abstract class CollectionValidatorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index 0927aedacdd5c..f5178cc029af7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\FileValidator; -use Symfony\Component\HttpFoundation\File\UploadedFile; abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php index dcc9c027bf75b..d58bf25ba6b2f 100644 --- a/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Validator\Tests; +use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ExecutionContext; -use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\ValidationVisitor; -use Symfony\Component\Validator\ConstraintValidatorFactory; class ExecutionContextTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index 852eb484b0ff1..e3f0d9a007800 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Tests\Fixtures; use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\MetadataInterface; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php index df2d9f4104ce8..f238a899d6176 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Tests\Mapping\Cache; -use Symfony\Component\Validator\Mapping\Cache\DoctrineCache; use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Validator\Mapping\Cache\DoctrineCache; class DoctrineCacheTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php index bee4025d0d279..aee137a1f8c02 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Validator\Tests\Mapping; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; -use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; class ClassMetadataFactoryTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php index 9ead7d134eb60..9579b36b5b560 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php @@ -14,8 +14,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Exception\GroupDefinitionException; -use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php index 8cf3e6dec4683..9539b0f22cfed 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Tests\Mapping; +use Symfony\Component\Validator\Mapping\ElementMetadata; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; -use Symfony\Component\Validator\Mapping\ElementMetadata; class ElementMetadataTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php index e4ea6cfc6a498..8da207ff9de57 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -14,10 +14,10 @@ use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\True; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php index 7723349e94d8e..09e6e449e0309 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; -use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; class FilesLoaderTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index e7243edc2c31f..e2b27f0bb6107 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -13,10 +13,10 @@ use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\True; use Symfony\Component\Validator\Mapping\ClassMetadata; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index 1de902a551a40..aeccf0c2836ab 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -13,10 +13,10 @@ use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\True; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php index bfb402cdee2be..f91088de0b016 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Tests\Mapping; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; -use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\MemberMetadata; +use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; class MemberMetadataTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index b1629ea50a470..2808d38137ac6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -13,14 +13,14 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ExecutionContextInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; use Symfony\Component\Validator\Tests\Fixtures\Reference; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Mapping\ClassMetadata; /** * @since 2.5 diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index eef4e84e7461b..7faeea6284e9c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -11,14 +11,10 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; +use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 494d9fc029e8e..581e6768399aa 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -11,14 +11,10 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; +use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php index 57134410dcb08..da43279638852 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php @@ -11,16 +11,11 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; +use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\RecursiveValidator; -use Symfony\Component\Validator\Validator\TraversingValidator; class RecursiveValidator2Dot5ApiTest extends Abstract2Dot5ApiTest { diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php index 7f96944b77ce3..5c76a173a4c53 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php @@ -11,14 +11,12 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; +use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\Validator\TraversingValidator; class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php index a983a78a70819..0f588fef8d3db 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -12,12 +12,12 @@ namespace Symfony\Component\Validator\Tests; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Validator\AbstractLegacyApiTest; use Symfony\Component\Validator\Validator as LegacyValidator; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; class ValidatorTest extends AbstractLegacyApiTest { diff --git a/src/Symfony/Component/Validator/ValidationVisitor.php b/src/Symfony/Component/Validator/ValidationVisitor.php index 302d33c9d7f43..fef687491f4b9 100644 --- a/src/Symfony/Component/Validator/ValidationVisitor.php +++ b/src/Symfony/Component/Validator/ValidationVisitor.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Translation\TranslatorInterface; /** * Default implementation of {@link ValidationVisitorInterface} and diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index 31dd4b9c384f5..849ef489fe56b 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Translation\TranslatorInterface; /** * Default implementation of {@link ValidatorInterface}. diff --git a/src/Symfony/Component/Validator/Validator/TraversingValidator.php b/src/Symfony/Component/Validator/Validator/TraversingValidator.php index 4352b3180f81a..8fe07f630fb1f 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingValidator.php @@ -13,8 +13,8 @@ use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** * Default implementation of {@link ValidatorInterface}. diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 4f536c15551c4..2d66fa9f6af5b 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -11,32 +11,32 @@ namespace Symfony\Component\Validator; +use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; +use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\Reader; +use Doctrine\Common\Cache\ArrayCache; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; -use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; -use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; +use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; -use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader; +use Symfony\Component\Validator\Mapping\Loader\LoaderChain; +use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader; -use Symfony\Component\Translation\TranslatorInterface; -use Doctrine\Common\Annotations\Reader; -use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\CachedReader; -use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; +use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; -use Symfony\Component\Validator\Validator as ValidatorV24; -use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator\LegacyValidator; +use Symfony\Component\Validator\Validator\TraversingValidator; +use Symfony\Component\Validator\Validator as ValidatorV24; /** * The default implementation of {@link ValidatorBuilderInterface}. @@ -378,6 +378,7 @@ public function getValidator() if (is_file($file)) { require_once $file; + return true; } } diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index d486faed127cc..e35822e2621d8 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Validator; +use Doctrine\Common\Annotations\Reader; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Validator\Mapping\Cache\CacheInterface; use Symfony\Component\Translation\TranslatorInterface; -use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Validator\Mapping\Cache\CacheInterface; /** * A configurable builder for ValidatorInterface objects. From 73c9cc58064fb7facece7e29662f984260dd4bc6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 17:45:55 +0100 Subject: [PATCH 081/323] [Validator] Optimized performance by calling spl_object_hash() only once per object --- .../RecursiveContextualValidator.php | 123 ++++++++++-------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index e3872b4232c3f..b1b4b714cd281 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -102,6 +102,8 @@ public function validate($value, $constraints = null, $groups = null) $this->traverseGenericNode( $value, + is_object($value) ? spl_object_hash($value) : null, + null, null, $metadata, $this->defaultPropertyPath, @@ -116,11 +118,12 @@ public function validate($value, $constraints = null, $groups = null) if (is_object($value)) { $this->cascadeObject( - $value, - $this->defaultPropertyPath, - $groups, - TraversalStrategy::IMPLICIT, - $this->context + $value, + spl_object_hash($value), + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context ); return $this; @@ -128,11 +131,11 @@ public function validate($value, $constraints = null, $groups = null) if (is_array($value)) { $this->cascadeCollection( - $value, - $this->defaultPropertyPath, - $groups, - TraversalStrategy::IMPLICIT, - $this->context + $value, + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context ); return $this; @@ -148,9 +151,9 @@ public function validate($value, $constraints = null, $groups = null) /** * {@inheritdoc} */ - public function validateProperty($object, $propertyName, $groups = null) + public function validateProperty($container, $propertyName, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($container); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -165,13 +168,16 @@ public function validateProperty($object, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $containerHash = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($object); + $propertyValue = $propertyMetadata->getPropertyValue($container); $this->traverseGenericNode( $propertyValue, - $object, + is_object($propertyValue) ? spl_object_hash($propertyValue) : null, + $container, + $containerHash, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -187,9 +193,9 @@ public function validateProperty($object, $propertyName, $groups = null) /** * {@inheritdoc} */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) + public function validatePropertyValue($container, $propertyName, $value, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($container); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -204,11 +210,14 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $containerHash = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { $this->traverseGenericNode( $value, - $object, + is_object($value) ? spl_object_hash($value) : null, + $container, + $containerHash, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -266,12 +275,12 @@ protected function normalizeGroups($groups) * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode($value, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { // Replace "Default" group by group sequence, if appropriate $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - $groups = $this->validateNode($value, $value, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { return; @@ -288,9 +297,13 @@ private function traverseClassNode($value, ClassMetadataInterface $metadata = nu )); } + $propertyValue = $propertyMetadata->getPropertyValue($value); + $this->traverseGenericNode( - $propertyMetadata->getPropertyValue($value), + $propertyValue, + is_object($propertyValue) ? spl_object_hash($propertyValue) : null, $value, + $valueHash, $propertyMetadata, $propertyPath ? $propertyPath.'.'.$propertyName @@ -392,6 +405,7 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t if (is_object($value)) { $this->cascadeObject( $value, + spl_object_hash($value), $propertyPath.'['.$key.']', $groups, $traversalStrategy, @@ -418,9 +432,9 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t * @param Node $node The node * @param ExecutionContextInterface $context The current execution context */ - private function traverseGenericNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function traverseGenericNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $object, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $groups = $this->validateNode($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { return; @@ -467,6 +481,7 @@ private function traverseGenericNode($value, $object, MetadataInterface $metadat // (BC with Symfony < 2.5) $this->cascadeObject( $value, + $valueHash, $propertyPath, $cascadedGroups, $traversalStrategy, @@ -492,7 +507,7 @@ private function traverseGenericNode($value, $object, MetadataInterface $metadat * traversal of the object, a new collection node is put on the stack. * Otherwise, an exception is thrown. * - * @param object $object The object to cascade + * @param object $container The object to cascade * @param string $propertyPath The current property path * @param string[] $groups The validated groups * @param integer $traversalStrategy The strategy for traversing the @@ -507,10 +522,10 @@ private function traverseGenericNode($value, $object, MetadataInterface $metadat * metadata factory does not implement * {@link ClassMetadataInterface} */ - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeObject($container, $containerHash, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { try { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($container); if (!$classMetadata instanceof ClassMetadataInterface) { throw new UnsupportedMetadataException(sprintf( @@ -522,7 +537,8 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal } $this->traverseClassNode( - $object, + $container, + $containerHash, $classMetadata, $propertyPath, $groups, @@ -532,7 +548,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal ); } catch (NoSuchMetadataException $e) { // Rethrow if not Traversable - if (!$object instanceof \Traversable) { + if (!$container instanceof \Traversable) { throw $e; } @@ -542,7 +558,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal } $this->cascadeCollection( - $object, + $container, $propertyPath, $groups, $traversalStrategy, @@ -563,14 +579,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal * * @return array The groups in which the successor nodes should be validated */ - public function validateNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { $context->setValue($value); $context->setMetadata($metadata); $context->setPropertyPath($propertyPath); - $objectHash = is_object($object) ? spl_object_hash($object) : null; - // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort // if necessary (G1, G2) @@ -587,7 +601,7 @@ public function validateNode($value, $object, MetadataInterface $metadata = null // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + if ($context->isObjectValidatedForGroup($valueHash, $groupHash)) { // Skip this group when validating the successor nodes // (property and/or collection nodes) unset($groups[$key]); @@ -595,12 +609,12 @@ public function validateNode($value, $object, MetadataInterface $metadata = null continue; } - $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markObjectAsValidatedForGroup($valueHash, $groupHash); } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); + $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $context); // Skip the group sequence when validating successor nodes unset($groups[$key]); @@ -609,7 +623,7 @@ public function validateNode($value, $object, MetadataInterface $metadata = null } // Validate normal group - $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); + $this->validateNodeForGroup($value, $valueHash, $containerHash, $metadata, $group, $context); } return $groups; @@ -625,7 +639,7 @@ public function validateNode($value, $object, MetadataInterface $metadata = null * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $object, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); @@ -640,6 +654,7 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me if ($metadata instanceof ClassMetadataInterface) { $this->traverseClassNode( $value, + $valueHash, $metadata, $propertyPath, $groups, @@ -650,7 +665,9 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me } else { $this->traverseGenericNode( $value, - $object, + $valueHash, + $container, + $containerHash, $metadata, $propertyPath, $groups, @@ -673,36 +690,38 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me * @param Node $node The validated node * @param string $group The group to validate * @param ExecutionContextInterface $context The execution context - * @param string $objectHash The hash of the node's + * @param string $containerHash The hash of the node's * object (if any) * * @throws \Exception */ - private function validateNodeForGroup($value, $objectHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) + private function validateNodeForGroup($value, $valueHash, $containerHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) { $context->setGroup($group); + $propertyName = $metadata instanceof PropertyMetadataInterface + ? $metadata->getPropertyName() + : null; + foreach ($metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $objectHash) { + if (null !== $propertyName) { $constraintHash = spl_object_hash($constraint); - if ($metadata instanceof ClassMetadataInterface) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } - - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($metadata instanceof PropertyMetadataInterface) { - $propertyName = $metadata->getPropertyName(); + if ($context->isPropertyConstraintValidated($containerHash, $propertyName, $constraintHash)) { + continue; + } - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } + $context->markPropertyConstraintAsValidated($containerHash, $propertyName, $constraintHash); + } elseif (null !== $valueHash) { + $constraintHash = spl_object_hash($constraint); - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isClassConstraintValidated($valueHash, $constraintHash)) { + continue; } + + $context->markClassConstraintAsValidated($valueHash, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); From 2f23d9725b3ccfbd990725c832319c50894ac52c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 17:55:15 +0100 Subject: [PATCH 082/323] [Validator] Reduced number of method calls on the execution context --- .../Validator/Context/ExecutionContext.php | 16 +----- .../Context/ExecutionContextInterface.php | 26 ++------- .../NodeVisitor/NodeValidationVisitor.php | 53 ++++++++----------- .../RecursiveContextualValidator.php | 4 +- 4 files changed, 27 insertions(+), 72 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 683ddd8c97329..718de5eb2585a 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -139,24 +139,10 @@ public function __construct(ValidatorInterface $validator, $root, TranslatorInte /** * {@inheritdoc} */ - public function setValue($value) + public function setNode($value, MetadataInterface $metadata = null, $propertyPath) { $this->value = $value; - } - - /** - * {@inheritdoc} - */ - public function setMetadata(MetadataInterface $metadata = null) - { $this->metadata = $metadata; - } - - /** - * {@inheritdoc} - */ - public function setPropertyPath($propertyPath) - { $this->propertyPath = (string) $propertyPath; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index b3224d5dd7a98..beafe75433c5c 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -102,32 +102,14 @@ public function getValidator(); /** * Sets the currently validated value. * - * @param mixed $value The validated value + * @param mixed $value The validated value + * @param MetadataInterface $metadata The validation metadata + * @param string $propertyPath The property path to the current value * * @internal Used by the validator engine. Should not be called by user * code. */ - public function setValue($value); - - /** - * Sets the current validation metadata. - * - * @param MetadataInterface $metadata The validation metadata - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function setMetadata(MetadataInterface $metadata = null); - - /** - * Sets the property path leading to the current value. - * - * @param string $propertyPath The property path to the current value - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function setPropertyPath($propertyPath); + public function setNode($value, MetadataInterface $metadata = null, $propertyPath); /** * Sets the currently validated group. diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 3a5a0f2bf8a33..d6e818c44a664 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -69,9 +69,7 @@ public function visit(Node $node, ExecutionContextInterface $context) return true; } - $context->setValue($node->value); - $context->setMetadata($node->metadata); - $context->setPropertyPath($node->propertyPath); + $context->setNode($node->value, $node->metadata, $node->propertyPath); if ($node instanceof ClassNode) { $this->replaceDefaultGroup($node); @@ -171,43 +169,34 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, */ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { - try { - $context->setGroup($group); + $context->setGroup($group); - foreach ($node->metadata->findConstraints($group) as $constraint) { - // Prevent duplicate validation of constraints, in the case - // that constraints belong to multiple validated groups - if (null !== $objectHash) { - $constraintHash = spl_object_hash($constraint); + foreach ($node->metadata->findConstraints($group) as $constraint) { + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups + if (null !== $objectHash) { + $constraintHash = spl_object_hash($constraint); - if ($node instanceof ClassNode) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } - - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($node instanceof PropertyNode) { - $propertyName = $node->metadata->getPropertyName(); + if ($node instanceof ClassNode) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($node instanceof PropertyNode) { + $propertyName = $node->metadata->getPropertyName(); - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; } - } - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($context); - $validator->validate($node->value, $constraint); + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + } } - $context->setGroup(null); - } catch (\Exception $e) { - // Should be put into a finally block once we switch to PHP 5.5 - $context->setGroup(null); - - throw $e; + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($context); + $validator->validate($node->value, $constraint); } } diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index b1b4b714cd281..563fe74288be5 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -581,9 +581,7 @@ private function cascadeObject($container, $containerHash, $propertyPath, array */ public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - $context->setValue($value); - $context->setMetadata($metadata); - $context->setPropertyPath($propertyPath); + $context->setNode($value, $metadata, $propertyPath); // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort From 029a71638e4ab3461e4636f7f1991632d496939a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 18:28:47 +0100 Subject: [PATCH 083/323] [Validator] Moved logic of replaceDefaultGroup() to validateNode() --- .../NodeVisitor/NodeValidationVisitor.php | 79 +++++------ .../RecursiveContextualValidator.php | 123 ++++++++---------- 2 files changed, 83 insertions(+), 119 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index d6e818c44a664..d3d7937ad93bd 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -72,8 +72,6 @@ public function visit(Node $node, ExecutionContextInterface $context) $context->setNode($node->value, $node->metadata, $node->propertyPath); if ($node instanceof ClassNode) { - $this->replaceDefaultGroup($node); - $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { $objectHash = spl_object_hash($node->object); @@ -88,6 +86,8 @@ public function visit(Node $node, ExecutionContextInterface $context) // simply continue traversal (if possible) foreach ($node->groups as $key => $group) { + $cascadedGroup = null; + // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). @@ -106,11 +106,36 @@ public function visit(Node $node, ExecutionContextInterface $context) } $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $node->metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $node->value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $context); + $this->traverseGroupSequence($node, $group, $cascadedGroup, $context); // Skip the group sequence when validating successor nodes unset($node->groups[$key]); @@ -135,17 +160,15 @@ public function visit(Node $node, ExecutionContextInterface $context) * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); + $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; foreach ($groupSequence->groups as $groupInSequence) { $node = clone $node; $node->groups = array($groupInSequence); - - if (null !== $groupSequence->cascadedGroup) { - $node->cascadedGroups = array($groupSequence->cascadedGroup); - } + $node->cascadedGroups = $cascadedGroups; $this->nodeTraverser->traverse(array($node), $context); @@ -199,44 +222,4 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $validator->validate($node->value, $constraint); } } - - /** - * Checks class nodes whether their "Default" group is replaced by a group - * sequence and adjusts the validation groups accordingly. - * - * If the "Default" group is replaced for a class node, and if the validated - * groups of the node contain the group "Default", that group is replaced by - * the group sequence specified in the class' metadata. - * - * @param ClassNode $node The node - */ - private function replaceDefaultGroup(ClassNode $node) - { - if ($node->metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $node->metadata->getGroupSequence(); - } elseif ($node->metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $node->value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } else { - // The "Default" group is not overridden. Quit. - return; - } - - $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); - - if (false !== $key) { - // Replace the "Default" group by the group sequence - $node->groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } } diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 563fe74288be5..bb5a0fff5e0c7 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -277,9 +277,6 @@ protected function normalizeGroups($groups) */ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - // Replace "Default" group by group sequence, if appropriate - $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { @@ -444,14 +441,13 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH return; } - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = count($cascadedGroups) > 0 - ? $cascadedGroups - : $groups; - $cascadingStrategy = $metadata->getCascadingStrategy(); + // Quit unless we have an array or a cascaded object + if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) { + return; + } + // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the node's metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { @@ -460,6 +456,12 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = count($cascadedGroups) > 0 + ? $cascadedGroups + : $groups; + if (is_array($value)) { // Arrays are always traversed, independent of the specified // traversal strategy @@ -475,21 +477,17 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH return; } - if ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $value, - $valueHash, - $propertyPath, - $cascadedGroups, - $traversalStrategy, - $context - ); - - return; - } + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $value, + $valueHash, + $propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); // Currently, the traversal strategy can only be TRAVERSE for a // generic node if the cascading strategy is CASCADE. Thus, traversable @@ -590,6 +588,8 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met // simply continue traversal (if possible) foreach ($groups as $key => $group) { + $cascadedGroup = null; + // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). @@ -608,11 +608,36 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met } $context->markObjectAsValidatedForGroup($valueHash, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $context); + $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); // Skip the group sequence when validating successor nodes unset($groups[$key]); @@ -637,17 +662,13 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); + $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; foreach ($groupSequence->groups as $groupInSequence) { $groups = array($groupInSequence); - $cascadedGroups = null; - - if (null !== $groupSequence->cascadedGroup) { - $cascadedGroups = array($groupSequence->cascadedGroup); - } if ($metadata instanceof ClassMetadataInterface) { $this->traverseClassNode( @@ -727,44 +748,4 @@ private function validateNodeForGroup($value, $valueHash, $containerHash, Metada $validator->validate($value, $constraint); } } - - /** - * @param $value - * @param ClassMetadataInterface $metadata - * @param array $groups - * - * @return array - */ - private function replaceDefaultGroup($value, ClassMetadataInterface $metadata, array $groups) - { - $groupSequence = null; - - if ($metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $metadata->getGroupSequence(); - } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } - - if (null !== $groupSequence) { - $key = array_search(Constraint::DEFAULT_GROUP, $groups); - - if (false !== $key) { - // Replace the "Default" group by the group sequence - $groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } - - return $groups; - } } From 3183aed7cdd669ee6ad6fc4715e820dad1ae46c6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 20:33:59 +0100 Subject: [PATCH 084/323] [Validator] Improved performance of cache key generation --- .../Validator/Context/ExecutionContext.php | 50 +-- .../Context/ExecutionContextInterface.php | 43 +-- .../Component/Validator/Node/ClassNode.php | 8 +- .../Validator/Node/CollectionNode.php | 1 + src/Symfony/Component/Validator/Node/Node.php | 5 +- .../Component/Validator/Node/PropertyNode.php | 14 +- .../NonRecursiveNodeTraverser.php | 3 +- .../NodeVisitor/NodeValidationVisitor.php | 36 +- .../Validator/Tests/Node/ClassNodeTest.php | 2 +- .../NonRecursiveNodeTraverserTest.php | 2 +- .../RecursiveContextualValidator.php | 332 ++++++++---------- .../TraversingContextualValidator.php | 8 +- 12 files changed, 198 insertions(+), 306 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 718de5eb2585a..75b7e2c3bf6bc 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -104,7 +104,7 @@ class ExecutionContext implements ExecutionContextInterface * * @var array */ - private $validatedClassConstraints = array(); + private $validatedConstraints = array(); /** * Stores which property constraint has been validated for which property. @@ -319,64 +319,36 @@ public function getMetadataFactory() /** * {@inheritdoc} */ - public function markObjectAsValidatedForGroup($objectHash, $groupHash) + public function markGroupAsValidated($cacheKey, $groupHash) { - if (!isset($this->validatedObjects[$objectHash])) { - $this->validatedObjects[$objectHash] = array(); + if (!isset($this->validatedObjects[$cacheKey])) { + $this->validatedObjects[$cacheKey] = array(); } - $this->validatedObjects[$objectHash][$groupHash] = true; + $this->validatedObjects[$cacheKey][$groupHash] = true; } /** * {@inheritdoc} */ - public function isObjectValidatedForGroup($objectHash, $groupHash) + public function isGroupValidated($cacheKey, $groupHash) { - return isset($this->validatedObjects[$objectHash][$groupHash]); + return isset($this->validatedObjects[$cacheKey][$groupHash]); } /** * {@inheritdoc} */ - public function markClassConstraintAsValidated($objectHash, $constraintHash) + public function markConstraintAsValidated($cacheKey, $constraintHash) { - if (!isset($this->validatedClassConstraints[$objectHash])) { - $this->validatedClassConstraints[$objectHash] = array(); - } - - $this->validatedClassConstraints[$objectHash][$constraintHash] = true; - } - - /** - * {@inheritdoc} - */ - public function isClassConstraintValidated($objectHash, $constraintHash) - { - return isset($this->validatedClassConstraints[$objectHash][$constraintHash]); - } - - /** - * {@inheritdoc} - */ - public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash) - { - if (!isset($this->validatedPropertyConstraints[$objectHash])) { - $this->validatedPropertyConstraints[$objectHash] = array(); - } - - if (!isset($this->validatedPropertyConstraints[$objectHash][$propertyName])) { - $this->validatedPropertyConstraints[$objectHash][$propertyName] = array(); - } - - $this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash] = true; + $this->validatedConstraints[$cacheKey.':'.$constraintHash] = true; } /** * {@inheritdoc} */ - public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash) + public function isConstraintValidated($cacheKey, $constraintHash) { - return isset($this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash]); + return isset($this->validatedConstraints[$cacheKey.':'.$constraintHash]); } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index beafe75433c5c..2e778fbec815f 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -124,19 +124,19 @@ public function setGroup($group); /** * Marks an object as validated in a specific validation group. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * * @internal Used by the validator engine. Should not be called by user * code. */ - public function markObjectAsValidatedForGroup($objectHash, $groupHash); + public function markGroupAsValidated($cacheKey, $groupHash); /** * Returns whether an object was validated in a specific validation group. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * @@ -146,23 +146,23 @@ public function markObjectAsValidatedForGroup($objectHash, $groupHash); * @internal Used by the validator engine. Should not be called by user * code. */ - public function isObjectValidatedForGroup($objectHash, $groupHash); + public function isGroupValidated($cacheKey, $groupHash); /** * Marks a constraint as validated for an object. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @internal Used by the validator engine. Should not be called by user * code. */ - public function markClassConstraintAsValidated($objectHash, $constraintHash); + public function markConstraintAsValidated($cacheKey, $constraintHash); /** * Returns whether a constraint was validated for an object. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @return Boolean Whether the constraint was already validated @@ -170,32 +170,5 @@ public function markClassConstraintAsValidated($objectHash, $constraintHash); * @internal Used by the validator engine. Should not be called by user * code. */ - public function isClassConstraintValidated($objectHash, $constraintHash); - - /** - * Marks a constraint as validated for an object and a property name. - * - * @param string $objectHash The hash of the object - * @param string $propertyName The property name - * @param string $constraintHash The hash of the constraint - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); - - /** - * Returns whether a constraint was validated for an object and a property - * name. - * - * @param string $objectHash The hash of the object - * @param string $propertyName The property name - * @param string $constraintHash The hash of the constraint - * - * @return Boolean Whether the constraint was already validated - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash); + public function isConstraintValidated($cacheKey, $constraintHash); } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 54e22e2d97403..f52a68366be0f 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -55,7 +55,7 @@ class ClassNode extends Node * * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($object, $cacheKey, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -63,12 +63,12 @@ public function __construct($object, ClassMetadataInterface $metadata, $property parent::__construct( $object, + $cacheKey, $metadata, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); - - $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index ddca97a5c7f09..79a49ff6e31f6 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -56,6 +56,7 @@ public function __construct($collection, $propertyPath, array $groups, $cascaded parent::__construct( $collection, null, + null, $propertyPath, $groups, $cascadedGroups, diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 56c8145c45682..93099844beed0 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -30,6 +30,8 @@ abstract class Node */ public $value; + public $cacheKey; + /** * The metadata specifying how the value should be validated. * @@ -82,13 +84,14 @@ abstract class Node * * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (null !== $cascadedGroups && !is_array($cascadedGroups)) { throw new UnexpectedTypeException($cascadedGroups, 'null or array'); } $this->value = $value; + $this->cacheKey = $cacheKey; $this->metadata = $metadata; $this->propertyPath = $propertyPath; $this->groups = $groups; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 8934bf1d7330c..4ee7ac5918906 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -41,11 +41,6 @@ */ class PropertyNode extends Node { - /** - * @var object - */ - public $object; - /** * @var PropertyMetadataInterface */ @@ -71,22 +66,17 @@ class PropertyNode extends Node * * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ - public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($value, $cacheKey, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { - if (!is_object($object)) { - throw new UnexpectedTypeException($object, 'object'); - } - parent::__construct( $value, + $cacheKey, $metadata, $propertyPath, $groups, $cascadedGroups, $traversalStrategy ); - - $this->object = $object; } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index 5c904f01d47f7..c29bac71e2387 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -274,8 +274,8 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c } $nodeStack->push(new PropertyNode( - $node->value, $propertyMetadata->getPropertyValue($node->value), + $node->cacheKey.':'.$propertyName, $propertyMetadata, $node->propertyPath ? $node->propertyPath.'.'.$propertyName @@ -530,6 +530,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $nodeStack->push(new ClassNode( $object, + spl_object_hash($object), $classMetadata, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index d3d7937ad93bd..5eee760c8a036 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -71,14 +71,6 @@ public function visit(Node $node, ExecutionContextInterface $context) $context->setNode($node->value, $node->metadata, $node->propertyPath); - if ($node instanceof ClassNode) { - $objectHash = spl_object_hash($node->value); - } elseif ($node instanceof PropertyNode) { - $objectHash = spl_object_hash($node->object); - } else { - $objectHash = null; - } - // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort // if necessary (G1, G2) @@ -97,7 +89,7 @@ public function visit(Node $node, ExecutionContextInterface $context) // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + if ($context->isGroupValidated($node->cacheKey, $groupHash)) { // Skip this group when validating the successor nodes // (property and/or collection nodes) unset($node->groups[$key]); @@ -105,7 +97,7 @@ public function visit(Node $node, ExecutionContextInterface $context) continue; } - $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markGroupAsValidated($node->cacheKey, $groupHash); // Replace the "Default" group by the group sequence defined // for the class, if applicable @@ -144,7 +136,7 @@ public function visit(Node $node, ExecutionContextInterface $context) } // Validate normal group - $this->validateNodeForGroup($node, $group, $context, $objectHash); + $this->validateInGroup($node, $group, $context); } return true; @@ -190,31 +182,21 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + private function validateInGroup(Node $node, $group, ExecutionContextInterface $context) { $context->setGroup($group); foreach ($node->metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $objectHash) { + if (null !== $node->cacheKey) { $constraintHash = spl_object_hash($constraint); - if ($node instanceof ClassNode) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } - - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($node instanceof PropertyNode) { - $propertyName = $node->metadata->getPropertyName(); - - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } - - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isConstraintValidated($node->cacheKey, $constraintHash)) { + continue; } + + $context->markConstraintAsValidated($node->cacheKey, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php index 1241d1bb5b830..c79f4c838f60c 100644 --- a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php +++ b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php @@ -26,6 +26,6 @@ public function testConstructorExpectsObject() { $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); - new ClassNode('foobar', $metadata, '', array(), array()); + new ClassNode('foobar', null, $metadata, '', array(), array()); } } diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php index 4dfc7071248e9..09e26bcaf9e11 100644 --- a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php +++ b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php @@ -40,7 +40,7 @@ protected function setUp() public function testVisitorsMayPreventTraversal() { - $nodes = array(new GenericNode('value', new GenericMetadata(), '', array('Default'))); + $nodes = array(new GenericNode('value', null, new GenericMetadata(), '', array('Default'))); $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index bb5a0fff5e0c7..0e083c05cf8fe 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -100,11 +100,9 @@ public function validate($value, $constraints = null, $groups = null) $metadata = new GenericMetadata(); $metadata->addConstraints($constraints); - $this->traverseGenericNode( + $this->validateGenericNode( $value, is_object($value) ? spl_object_hash($value) : null, - null, - null, $metadata, $this->defaultPropertyPath, $groups, @@ -119,7 +117,6 @@ public function validate($value, $constraints = null, $groups = null) if (is_object($value)) { $this->cascadeObject( $value, - spl_object_hash($value), $this->defaultPropertyPath, $groups, TraversalStrategy::IMPLICIT, @@ -168,16 +165,14 @@ public function validateProperty($container, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $containerHash = spl_object_hash($container); + $cacheKey = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($container); - $this->traverseGenericNode( + $this->validateGenericNode( $propertyValue, - is_object($propertyValue) ? spl_object_hash($propertyValue) : null, - $container, - $containerHash, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -210,14 +205,12 @@ public function validatePropertyValue($container, $propertyName, $value, $groups $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $containerHash = spl_object_hash($container); + $cacheKey = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { - $this->traverseGenericNode( + $this->validateGenericNode( $value, - is_object($value) ? spl_object_hash($value) : null, - $container, - $containerHash, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -275,9 +268,74 @@ protected function normalizeGroups($groups) * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateClassNode($value, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $context->setNode($value, $metadata, $propertyPath); + + // if group (=[,G3,G4]) contains group sequence (=) + // then call traverse() with each entry of the group sequence and abort + // if necessary (G1, G2) + // finally call traverse() with remaining entries ([G3,G4]) or + // simply continue traversal (if possible) + + foreach ($groups as $key => $group) { + $cascadedGroup = null; + + // Even if we remove the following clause, the constraints on an + // object won't be validated again due to the measures taken in + // validateNodeForGroup(). + // The following shortcut, however, prevents validatedNodeForGroup() + // from being called at all and enhances performance a bit. + + // Use the object hash for group sequences + $groupHash = is_object($group) ? spl_object_hash($group) : $group; + + if ($context->isGroupValidated($cacheKey, $groupHash)) { + // Skip this group when validating the successor nodes + // (property and/or collection nodes) + unset($groups[$key]); + + continue; + } + + $context->markGroupAsValidated($cacheKey, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } + + if ($group instanceof GroupSequence) { + $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); + + continue; + } + + $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + } if (0 === count($groups)) { return; @@ -296,11 +354,9 @@ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $m $propertyValue = $propertyMetadata->getPropertyValue($value); - $this->traverseGenericNode( + $this->validateGenericNode( $propertyValue, - is_object($propertyValue) ? spl_object_hash($propertyValue) : null, - $value, - $valueHash, + $cacheKey.':'.$propertyName, $propertyMetadata, $propertyPath ? $propertyPath.'.'.$propertyName @@ -351,67 +407,6 @@ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $m ); } - /** - * Traverses a collection node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: - * - * - for each object in the collection with associated class metadata, a - * new class node is put on the stack; - * - if an object has no associated class metadata, but is traversable, and - * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for - * collection node, a new collection node is put on the stack for that - * object; - * - for each array in the collection, a new collection node is put on the - * stack. - * - * @param CollectionNode $node The collection node - * @param ExecutionContextInterface $context The current execution context - * - * @see ClassNode - * @see CollectionNode - */ - private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { - $traversalStrategy = TraversalStrategy::NONE; - } else { - $traversalStrategy = TraversalStrategy::IMPLICIT; - } - - foreach ($collection as $key => $value) { - if (is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $this->cascadeCollection( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - spl_object_hash($value), - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - } - } - } - /** * Traverses a node that is neither a class nor a collection node. * @@ -429,9 +424,22 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t * @param Node $node The node * @param ExecutionContextInterface $context The current execution context */ - private function traverseGenericNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateGenericNode($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $context->setNode($value, $metadata, $propertyPath); + + foreach ($groups as $key => $group) { + if ($group instanceof GroupSequence) { + $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, null, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); + + continue; + } + + $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + } if (0 === count($groups)) { return; @@ -482,7 +490,6 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH // (BC with Symfony < 2.5) $this->cascadeObject( $value, - $valueHash, $propertyPath, $cascadedGroups, $traversalStrategy, @@ -520,7 +527,7 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH * metadata factory does not implement * {@link ClassMetadataInterface} */ - private function cascadeObject($container, $containerHash, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeObject($container, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { try { $classMetadata = $this->metadataFactory->getMetadataFor($container); @@ -534,9 +541,9 @@ private function cascadeObject($container, $containerHash, $propertyPath, array )); } - $this->traverseClassNode( + $this->validateClassNode( $container, - $containerHash, + spl_object_hash($container), $classMetadata, $propertyPath, $groups, @@ -566,90 +573,63 @@ private function cascadeObject($container, $containerHash, $propertyPath, array } /** - * Validates a node's value against the constraints defined in the node's - * metadata. + * Traverses a collection node. * - * Objects and constraints that were validated before in the same context - * will be skipped. + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context + * - for each object in the collection with associated class metadata, a + * new class node is put on the stack; + * - if an object has no associated class metadata, but is traversable, and + * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for + * collection node, a new collection node is put on the stack for that + * object; + * - for each array in the collection, a new collection node is put on the + * stack. + * + * @param CollectionNode $node The collection node + * @param ExecutionContextInterface $context The current execution context * - * @return array The groups in which the successor nodes should be validated + * @see ClassNode + * @see CollectionNode */ - public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - $context->setNode($value, $metadata, $propertyPath); - - // if group (=[,G3,G4]) contains group sequence (=) - // then call traverse() with each entry of the group sequence and abort - // if necessary (G1, G2) - // finally call traverse() with remaining entries ([G3,G4]) or - // simply continue traversal (if possible) - - foreach ($groups as $key => $group) { - $cascadedGroup = null; - - // Even if we remove the following clause, the constraints on an - // object won't be validated again due to the measures taken in - // validateNodeForGroup(). - // The following shortcut, however, prevents validatedNodeForGroup() - // from being called at all and enhances performance a bit. - if ($metadata instanceof ClassMetadataInterface) { - // Use the object hash for group sequences - $groupHash = is_object($group) ? spl_object_hash($group) : $group; - - if ($context->isObjectValidatedForGroup($valueHash, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) - unset($groups[$key]); - - continue; - } - - $context->markObjectAsValidatedForGroup($valueHash, $groupHash); - - // Replace the "Default" group by the group sequence defined - // for the class, if applicable - // This is done after checking the cache, so that - // spl_object_hash() isn't called for this sequence and - // "Default" is used instead in the cache. This is useful - // if the getters below return different group sequences in - // every call. - if (Constraint::DEFAULT_GROUP === $group) { - if ($metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $group = $metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - - if (!$group instanceof GroupSequence) { - $group = new GroupSequence($group); - } - } - } - } - - if ($group instanceof GroupSequence) { - // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { + $traversalStrategy = TraversalStrategy::NONE; + } else { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } - // Skip the group sequence when validating successor nodes - unset($groups[$key]); + foreach ($collection as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeCollection( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); continue; } - // Validate normal group - $this->validateNodeForGroup($value, $valueHash, $containerHash, $metadata, $group, $context); + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->cascadeObject( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); + } } - - return $groups; } /** @@ -662,7 +642,7 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; @@ -671,9 +651,9 @@ private function stepThroughGroupSequence($value, $valueHash, $container, $conta $groups = array($groupInSequence); if ($metadata instanceof ClassMetadataInterface) { - $this->traverseClassNode( + $this->validateClassNode( $value, - $valueHash, + $cacheKey, $metadata, $propertyPath, $groups, @@ -682,11 +662,9 @@ private function stepThroughGroupSequence($value, $valueHash, $container, $conta $context ); } else { - $this->traverseGenericNode( + $this->validateGenericNode( $value, - $valueHash, - $container, - $containerHash, + $cacheKey, $metadata, $propertyPath, $groups, @@ -714,33 +692,21 @@ private function stepThroughGroupSequence($value, $valueHash, $container, $conta * * @throws \Exception */ - private function validateNodeForGroup($value, $valueHash, $containerHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) + private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context) { $context->setGroup($group); - $propertyName = $metadata instanceof PropertyMetadataInterface - ? $metadata->getPropertyName() - : null; - foreach ($metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $propertyName) { - $constraintHash = spl_object_hash($constraint); - - if ($context->isPropertyConstraintValidated($containerHash, $propertyName, $constraintHash)) { - continue; - } - - $context->markPropertyConstraintAsValidated($containerHash, $propertyName, $constraintHash); - } elseif (null !== $valueHash) { + if (null !== $cacheKey) { $constraintHash = spl_object_hash($constraint); - if ($context->isClassConstraintValidated($valueHash, $constraintHash)) { + if ($context->isConstraintValidated($cacheKey, $constraintHash)) { continue; } - $context->markClassConstraintAsValidated($valueHash, $constraintHash); + $context->markConstraintAsValidated($cacheKey, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index 288fd8a66c603..bd749eeb9757f 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -94,6 +94,7 @@ public function validate($value, $constraints = null, $groups = null) $node = new GenericNode( $value, + is_object($value) ? spl_object_hash($value) : null, $metadata, $this->defaultPropertyPath, $groups @@ -118,6 +119,7 @@ public function validate($value, $constraints = null, $groups = null) $node = new ClassNode( $value, + spl_object_hash($value), $metadata, $this->defaultPropertyPath, $groups @@ -155,14 +157,15 @@ public function validateProperty($object, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $cacheKey = spl_object_hash($object); $nodes = array(); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($object); $nodes[] = new PropertyNode( - $object, $propertyValue, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups @@ -194,12 +197,13 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $cacheKey = spl_object_hash($object); $nodes = array(); foreach ($propertyMetadatas as $propertyMetadata) { $nodes[] = new PropertyNode( - $object, $value, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, From 90c27bb1e729a3b865f87b79c5f75b97d9c5645c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 10:02:15 +0100 Subject: [PATCH 085/323] [Validator] Removed traverser implementation The traverser is too slow compared to the current, recursive approach. Testing showed a performance decrease of about 70% without a lot of optimization potential. --- .../Component/Validator/Node/ClassNode.php | 74 --- .../Validator/Node/CollectionNode.php | 66 --- .../Component/Validator/Node/GenericNode.php | 26 - src/Symfony/Component/Validator/Node/Node.php | 101 ---- .../Component/Validator/Node/PropertyNode.php | 82 --- .../NodeTraverser/NodeTraverserInterface.php | 99 ---- .../NonRecursiveNodeTraverser.php | 560 ------------------ .../Validator/NodeVisitor/AbstractVisitor.php | 47 -- .../NodeVisitor/NodeValidationVisitor.php | 207 ------- .../NodeVisitor/NodeVisitorInterface.php | 59 -- .../ObjectInitializationVisitor.php | 83 --- .../Validator/Tests/Node/ClassNodeTest.php | 31 - .../NonRecursiveNodeTraverserTest.php | 91 --- .../TraversingValidator2Dot5ApiTest.php | 34 -- .../Validator/Tests/ValidatorBuilderTest.php | 2 +- .../TraversingContextualValidator.php | 242 -------- .../Validator/TraversingValidator.php | 128 ---- .../Component/Validator/ValidatorBuilder.php | 9 +- 18 files changed, 3 insertions(+), 1938 deletions(-) delete mode 100644 src/Symfony/Component/Validator/Node/ClassNode.php delete mode 100644 src/Symfony/Component/Validator/Node/CollectionNode.php delete mode 100644 src/Symfony/Component/Validator/Node/GenericNode.php delete mode 100644 src/Symfony/Component/Validator/Node/Node.php delete mode 100644 src/Symfony/Component/Validator/Node/PropertyNode.php delete mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php delete mode 100644 src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php delete mode 100644 src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php delete mode 100644 src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php delete mode 100644 src/Symfony/Component/Validator/Validator/TraversingValidator.php diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php deleted file mode 100644 index f52a68366be0f..0000000000000 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ /dev/null @@ -1,74 +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\Node; - -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * Represents an object and its class metadata in the validation graph. - * - * If the object is a collection which should be traversed, a new - * {@link CollectionNode} instance will be created for that object: - * - * (TagList:ClassNode) - * \ - * (TagList:CollectionNode) - * - * @since 2.5 - * @author Bernhard Schussek - */ -class ClassNode extends Node -{ - /** - * @var ClassMetadataInterface - */ - public $metadata; - - /** - * Creates a new class node. - * - * @param object $object The validated object - * @param ClassMetadataInterface $metadata The class metadata of - * that object - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should - * be validated - * @param integer $traversalStrategy The strategy used for - * traversing the object - * - * @throws UnexpectedTypeException If $object is not an object - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public function __construct($object, $cacheKey, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) - { - if (!is_object($object)) { - throw new UnexpectedTypeException($object, 'object'); - } - - parent::__construct( - $object, - $cacheKey, - $metadata, - $propertyPath, - $groups, - $cascadedGroups, - $traversalStrategy - ); - } -} diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php deleted file mode 100644 index 79a49ff6e31f6..0000000000000 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ /dev/null @@ -1,66 +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\Node; - -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * Represents a traversable value in the validation graph. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class CollectionNode extends Node -{ - /** - * Creates a new collection node. - * - * @param array|\Traversable $collection The validated collection - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should be - * validated - * @param integer $traversalStrategy The strategy used for - * traversing the collection - * - * @throws ConstraintDefinitionException If $collection is not an array or a - * \Traversable - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE) - { - if (!is_array($collection) && !$collection instanceof \Traversable) { - // Must throw a ConstraintDefinitionException for backwards - // compatibility reasons with Symfony < 2.5 - throw new ConstraintDefinitionException(sprintf( - 'Traversal was enabled for "%s", but this class '. - 'does not implement "\Traversable".', - get_class($collection) - )); - } - - parent::__construct( - $collection, - null, - null, - $propertyPath, - $groups, - $cascadedGroups, - $traversalStrategy - ); - } -} diff --git a/src/Symfony/Component/Validator/Node/GenericNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php deleted file mode 100644 index 9c628e0a465e0..0000000000000 --- a/src/Symfony/Component/Validator/Node/GenericNode.php +++ /dev/null @@ -1,26 +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\Node; - -/** - * Represents a value that has neither class metadata nor property metadata - * attached to it. - * - * Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, - * this node type can be used to validate a value against some constraints. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class GenericNode extends Node -{ -} diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php deleted file mode 100644 index 93099844beed0..0000000000000 --- a/src/Symfony/Component/Validator/Node/Node.php +++ /dev/null @@ -1,101 +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\Node; - -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\MetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * A node in the validation graph. - * - * @since 2.5 - * @author Bernhard Schussek - */ -abstract class Node -{ - /** - * The validated value. - * - * @var mixed - */ - public $value; - - public $cacheKey; - - /** - * The metadata specifying how the value should be validated. - * - * @var MetadataInterface|null - */ - public $metadata; - - /** - * The property path leading to this node. - * - * @var string - */ - public $propertyPath; - - /** - * The groups in which the value should be validated. - * - * @var string[] - */ - public $groups; - - /** - * The groups in which cascaded values should be validated. - * - * @var string[] - */ - public $cascadedGroups; - - /** - * The strategy used for traversing the validated value. - * - * @var integer - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public $traversalStrategy; - - /** - * Creates a new property node. - * - * @param mixed $value The property value - * @param MetadataInterface|null $metadata The property's metadata - * @param string $propertyPath The property path leading to - * this node - * @param string[] $groups The groups in which this node - * should be validated - * @param string[]|null $cascadedGroups The groups in which cascaded - * objects should be validated - * @param integer $traversalStrategy - * - * @throws UnexpectedTypeException If $cascadedGroups is invalid - */ - public function __construct($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) - { - if (null !== $cascadedGroups && !is_array($cascadedGroups)) { - throw new UnexpectedTypeException($cascadedGroups, 'null or array'); - } - - $this->value = $value; - $this->cacheKey = $cacheKey; - $this->metadata = $metadata; - $this->propertyPath = $propertyPath; - $this->groups = $groups; - $this->cascadedGroups = $cascadedGroups; - $this->traversalStrategy = $traversalStrategy; - } -} diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php deleted file mode 100644 index 4ee7ac5918906..0000000000000 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ /dev/null @@ -1,82 +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\Node; - -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * Represents the value of a property and its associated metadata. - * - * If the property contains an object and should be cascaded, a new - * {@link ClassNode} instance will be created for that object: - * - * (Article:ClassNode) - * \ - * (->author:PropertyNode) - * \ - * (Author:ClassNode) - * - * If the property contains a collection which should be traversed, a new - * {@link CollectionNode} instance will be created for that collection: - * - * (Article:ClassNode) - * \ - * (->tags:PropertyNode) - * \ - * (array:CollectionNode) - * - * @since 2.5 - * @author Bernhard Schussek - */ -class PropertyNode extends Node -{ - /** - * @var PropertyMetadataInterface - */ - public $metadata; - - /** - * Creates a new property node. - * - * @param object $object The object the property - * belongs to - * @param mixed $value The property value - * @param PropertyMetadataInterface $metadata The property's metadata - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should - * be validated - * @param integer $traversalStrategy - * - * @throws UnexpectedTypeException If $object is not an object - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public function __construct($value, $cacheKey, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) - { - parent::__construct( - $value, - $cacheKey, - $metadata, - $propertyPath, - $groups, - $cascadedGroups, - $traversalStrategy - ); - } - -} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php deleted file mode 100644 index 36e559ba6854e..0000000000000 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ /dev/null @@ -1,99 +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\NodeTraverser; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; - -/** - * Traverses the nodes of the validation graph. - * - * You can attach visitors to the traverser that are invoked during the - * traversal. Before starting the traversal, the - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} - * method of each visitor is called. For each node in the graph, the - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()} - * of each visitor is called. At the end of the traversal, the traverser invokes - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} - * on each visitor. The visitors are called in the same order in which they are - * added to the traverser. - * - * If the {@link traverse()} method is called recursively, the - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} - * and {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} - * methods of the visitors will be invoked for each call. - * - * The validation graph typically contains nodes of the following types: - * - * - {@link \Symfony\Component\Validator\Node\ClassNode}: - * An object with associated class metadata - * - {@link \Symfony\Component\Validator\Node\PropertyNode}: - * A property value with associated property metadata - * - {@link \Symfony\Component\Validator\Node\GenericNode}: - * A generic value with associated constraints - * - {@link \Symfony\Component\Validator\Node\CollectionNode}: - * A traversable collection - * - * Generic nodes are mostly useful when you want to validate a value that has - * neither associated class nor property metadata. Generic nodes usually come - * with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, that - * contains the constraints that the value should be validated against. - * - * Whenever a class, property or generic node is validated that contains a - * traversable value which should be traversed (according to the - * {@link \Symfony\Component\Validator\Mapping\TraversalStrategy} specified - * in the node or its metadata), a new - * {@link \Symfony\Component\Validator\Node\CollectionNode} will be attached - * to the node graph. - * - * For example: - * - * (TagList:ClassNode) - * \ - * (TagList:CollectionNode) - * - * When writing custom visitors, be aware that collection nodes usually contain - * values that have already been passed to the visitor before through a class - * node, a property node or a generic node. - * - * @since 2.5 - * @author Bernhard Schussek - */ -interface NodeTraverserInterface -{ - /** - * Adds a new visitor to the traverser. - * - * Visitors that have already been added before are ignored. - * - * @param NodeVisitorInterface $visitor The visitor to add - */ - public function addVisitor(NodeVisitorInterface $visitor); - - /** - * Removes a visitor from the traverser. - * - * Non-existing visitors are ignored. - * - * @param NodeVisitorInterface $visitor The visitor to remove - */ - public function removeVisitor(NodeVisitorInterface $visitor); - - /** - * Traverses the given nodes in the given context. - * - * @param Node[] $nodes The nodes to traverse - * @param ExecutionContextInterface $context The validation context - */ - public function traverse($nodes, ExecutionContextInterface $context); -} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php deleted file mode 100644 index c29bac71e2387..0000000000000 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ /dev/null @@ -1,560 +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\NodeTraverser; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Exception\UnsupportedMetadataException; -use Symfony\Component\Validator\Mapping\CascadingStrategy; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; - -/** - * Non-recursive implementation of {@link NodeTraverserInterface}. - * - * This implementation uses a Depth First algorithm to traverse the node - * graph. Instead of loading the complete node graph into memory before the - * traversal, the traverser only expands the successor nodes of a node once - * that node is traversed. For example, when traversing a class node, the - * nodes for all constrained properties of that class are loaded into memory. - * When the traversal of the class node is over, the node is discarded. - * - * Next, one of the class' property nodes is traversed. At that point, the - * successor nodes of that property node (a class node, if the property should - * be cascaded, or a collection node, if the property should be traversed) are - * loaded into memory. As soon as the traversal of the property node is over, - * it is discarded as well. - * - * This leads to an average memory consumption of O(log N * B), where N is the - * number of nodes in the graph and B is the average number of successor nodes - * of a node. - * - * In order to maintain a small execution stack, nodes are not validated - * recursively, but iteratively. Internally, a stack is used to store all the - * nodes that should be processed. Whenever a node is traversed, its successor - * nodes are put on the stack. The traverser keeps fetching and traversing nodes - * from the stack until the stack is empty and all nodes have been traversed. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see NodeTraverserInterface - * @see CascadingStrategy - * @see TraversalStrategy - */ -class NonRecursiveNodeTraverser implements NodeTraverserInterface -{ - /** - * @var NodeVisitorInterface[] - */ - private $visitors; - - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - - /** - * Creates a new traverser. - * - * @param MetadataFactoryInterface $metadataFactory The metadata factory - */ - public function __construct(MetadataFactoryInterface $metadataFactory) - { - $this->visitors = new \SplObjectStorage(); - $this->nodeStack = new \SplStack(); - $this->metadataFactory = $metadataFactory; - } - - /** - * {@inheritdoc} - */ - public function addVisitor(NodeVisitorInterface $visitor) - { - $this->visitors->attach($visitor); - } - - /** - * {@inheritdoc} - */ - public function removeVisitor(NodeVisitorInterface $visitor) - { - $this->visitors->detach($visitor); - } - - /** - * {@inheritdoc} - */ - public function traverse($nodes, ExecutionContextInterface $context) - { - if (!is_array($nodes)) { - $nodes = array($nodes); - } - - $numberOfInitializedVisitors = $this->beforeTraversal($nodes, $context); - - // If any of the visitors requested to abort the traversal, do so, but - // clean up before - if ($numberOfInitializedVisitors < count($this->visitors)) { - $this->afterTraversal($nodes, $context, $numberOfInitializedVisitors); - - return; - } - - // This stack contains all the nodes that should be traversed - // A stack is used rather than a queue in order to traverse the graph - // in a Depth First approach (the last added node is processed first). - // In this way, the order in which the nodes are passed to the visitors - // is similar to a recursive implementation (except that the successor - // nodes of a node are traversed right-to-left instead of left-to-right). - $nodeStack = new \SplStack(); - - foreach ($nodes as $node) { - // Push a node to the stack and immediately process it. This way, - // the successor nodes are traversed before the next node in $nodes - $nodeStack->push($node); - - // Fetch nodes from the stack and traverse them until no more nodes - // are left. Then continue with the next node in $nodes. - while (!$nodeStack->isEmpty()) { - $node = $nodeStack->pop(); - - if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $context, $nodeStack); - } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $context, $nodeStack); - } else { - $this->traverseNode($node, $context, $nodeStack); - } - } - } - - $this->afterTraversal($nodes, $context); - } - - /** - * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of - * each visitor. - * - * @param Node[] $nodes The traversed nodes - * @param ExecutionContextInterface $context The current execution context - * - * @return integer The number of successful calls. This is lower than - * the number of visitors if any of the visitors' - * beforeTraversal() methods returned false - */ - private function beforeTraversal($nodes, ExecutionContextInterface $context) - { - $numberOfCalls = 1; - - foreach ($this->visitors as $visitor) { - if (false === $visitor->beforeTraversal($nodes, $context)) { - break; - } - - ++$numberOfCalls; - } - - return $numberOfCalls; - } - - /** - * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of - * each visitor. - * - * @param Node[] $nodes The traversed nodes - * @param ExecutionContextInterface $context The current execution context - * @param integer|null $limit Limits the number of visitors - * on which beforeTraversal() - * should be called. All visitors - * will be called by default - */ - private function afterTraversal($nodes, ExecutionContextInterface $context, $limit = null) - { - if (null === $limit) { - $limit = count($this->visitors); - } - - $numberOfCalls = 0; - - foreach ($this->visitors as $visitor) { - $visitor->afterTraversal($nodes, $context); - - if (++$numberOfCalls === $limit) { - return; - } - } - } - - /** - * Executes the {@link NodeVisitorInterface::visit()} method of each - * visitor. - * - * @param Node $node The visited node - * @param ExecutionContextInterface $context The current execution context - * - * @return Boolean Whether to traverse the node's successor nodes - */ - private function visit(Node $node, ExecutionContextInterface $context) - { - foreach ($this->visitors as $visitor) { - if (false === $visitor->visit($node, $context)) { - return false; - } - } - - return true; - } - - /** - * Traverses a class node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, a property - * node is put on the node stack for each constrained property of the class. - * At last, if the class is traversable and should be traversed according - * to the selected traversal strategy, a new collection node is put on the - * stack. - * - * @param ClassNode $node The class node - * @param ExecutionContextInterface $context The current execution context - * @param \SplStack $nodeStack The stack for storing the - * successor nodes - * - * @throws UnsupportedMetadataException If a property metadata does not - * implement {@link PropertyMetadataInterface} - * - * @see ClassNode - * @see PropertyNode - * @see CollectionNode - * @see TraversalStrategy - */ - private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (0 === count($node->groups)) { - return; - } - - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - if (!$propertyMetadata instanceof PropertyMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The property metadata instances should implement '. - '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. - 'got: "%s".', - is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) - )); - } - - $nodeStack->push(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $node->cacheKey.':'.$propertyName, - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); - } - } - - $traversalStrategy = $node->traversalStrategy; - - // If no specific traversal strategy was requested when this method - // was called, use the traversal strategy of the class' metadata - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - // Keep the STOP_RECURSION flag, if it was set - $traversalStrategy = $node->metadata->getTraversalStrategy() - | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); - } - - // Traverse only if IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - return; - } - - // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { - return; - } - - // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $node->groups, - $node->cascadedGroups, - $traversalStrategy - )); - } - - /** - * Traverses a collection node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: - * - * - for each object in the collection with associated class metadata, a - * new class node is put on the stack; - * - if an object has no associated class metadata, but is traversable, and - * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for - * collection node, a new collection node is put on the stack for that - * object; - * - for each array in the collection, a new collection node is put on the - * stack. - * - * @param CollectionNode $node The collection node - * @param ExecutionContextInterface $context The current execution context - * @param \SplStack $nodeStack The stack for storing the - * successor nodes - * - * @see ClassNode - * @see CollectionNode - */ - private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (0 === count($node->groups)) { - return; - } - - $traversalStrategy = $node->traversalStrategy; - - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { - $traversalStrategy = TraversalStrategy::NONE; - } else { - $traversalStrategy = TraversalStrategy::IMPLICIT; - } - - foreach ($node->value as $key => $value) { - if (is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $nodeStack->push(new CollectionNode( - $value, - $node->propertyPath.'['.$key.']', - $node->groups, - null, - $traversalStrategy - )); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - $node->propertyPath.'['.$key.']', - $node->groups, - $traversalStrategy, - $nodeStack - ); - } - } - } - - /** - * Traverses a node that is neither a class nor a collection node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: - * - * - if the node contains an object with associated class metadata, a new - * class node is put on the stack; - * - if the node contains a traversable object without associated class - * metadata and traversal is enabled according to the selected traversal - * strategy, a collection node is put on the stack; - * - if the node contains an array, a collection node is put on the stack. - * - * @param Node $node The node - * @param ExecutionContextInterface $context The current execution context - * @param \SplStack $nodeStack The stack for storing the - * successor nodes - */ - private function traverseNode(Node $node, ExecutionContextInterface $context, \SplStack $nodeStack) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (null === $node->value) { - return; - } - - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = null !== $node->cascadedGroups - ? $node->cascadedGroups - : $node->groups; - - if (0 === count($cascadedGroups)) { - return; - } - - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->traversalStrategy; - - // If no specific traversal strategy was requested when this method - // was called, use the traversal strategy of the node's metadata - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - // Keep the STOP_RECURSION flag, if it was set - $traversalStrategy = $node->metadata->getTraversalStrategy() - | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); - } - - if (is_array($node->value)) { - // Arrays are always traversed, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); - - return; - } - - if ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $node->value, - $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $nodeStack - ); - - return; - } - - // Currently, the traversal strategy can only be TRAVERSE for a - // generic node if the cascading strategy is CASCADE. Thus, traversable - // objects will always be handled within cascadeObject() and there's - // nothing more to do here. - - // see GenericMetadata::addConstraint() - } - - /** - * Executes the cascading logic for an object. - * - * If class metadata is available for the object, a class node is put on - * the node stack. Otherwise, if the selected traversal strategy allows - * traversal of the object, a new collection node is put on the stack. - * Otherwise, an exception is thrown. - * - * @param object $object The object to cascade - * @param string $propertyPath The current property path - * @param string[] $groups The validated groups - * @param integer $traversalStrategy The strategy for traversing the - * cascaded object - * @param \SplStack $nodeStack The stack for storing the successor - * nodes - * - * @throws NoSuchMetadataException If the object has no associated metadata - * and does not implement {@link \Traversable} - * or if traversal is disabled via the - * $traversalStrategy argument - * @throws UnsupportedMetadataException If the metadata returned by the - * metadata factory does not implement - * {@link ClassMetadataInterface} - */ - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) - { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The metadata factory should return instances of '. - '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $nodeStack->push(new ClassNode( - $object, - spl_object_hash($object), - $classMetadata, - $propertyPath, - $groups, - null, - $traversalStrategy - )); - } catch (NoSuchMetadataException $e) { - // Rethrow if not Traversable - if (!$object instanceof \Traversable) { - throw $e; - } - - // Rethrow unless IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - throw $e; - } - - $nodeStack->push(new CollectionNode( - $object, - $propertyPath, - $groups, - null, - $traversalStrategy - )); - } - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php deleted file mode 100644 index c516f4a8d233b..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ /dev/null @@ -1,47 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; - -/** - * Base visitor with empty method stubs. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see NodeVisitorInterface - */ -abstract class AbstractVisitor implements NodeVisitorInterface -{ - /** - * {@inheritdoc} - */ - public function beforeTraversal($nodes, ExecutionContextInterface $context) - { - } - - /** - * {@inheritdoc} - */ - public function afterTraversal($nodes, ExecutionContextInterface $context) - { - } - - /** - * {@inheritdoc} - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php deleted file mode 100644 index 5eee760c8a036..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ /dev/null @@ -1,207 +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\NodeVisitor; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; - -/** - * Validates a node's value against the constraints defined in it's metadata. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class NodeValidationVisitor extends AbstractVisitor -{ - /** - * @var ConstraintValidatorFactoryInterface - */ - private $validatorFactory; - - /** - * @var NodeTraverserInterface - */ - private $nodeTraverser; - - /** - * Creates a new visitor. - * - * @param NodeTraverserInterface $nodeTraverser The node traverser - * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory - */ - public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) - { - $this->validatorFactory = $validatorFactory; - $this->nodeTraverser = $nodeTraverser; - } - - /** - * Validates a node's value against the constraints defined in the node's - * metadata. - * - * Objects and constraints that were validated before in the same context - * will be skipped. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Whether to traverse the successor nodes - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - if ($node instanceof CollectionNode) { - return true; - } - - $context->setNode($node->value, $node->metadata, $node->propertyPath); - - // if group (=[,G3,G4]) contains group sequence (=) - // then call traverse() with each entry of the group sequence and abort - // if necessary (G1, G2) - // finally call traverse() with remaining entries ([G3,G4]) or - // simply continue traversal (if possible) - - foreach ($node->groups as $key => $group) { - $cascadedGroup = null; - - // Even if we remove the following clause, the constraints on an - // object won't be validated again due to the measures taken in - // validateNodeForGroup(). - // The following shortcut, however, prevents validatedNodeForGroup() - // from being called at all and enhances performance a bit. - if ($node instanceof ClassNode) { - // Use the object hash for group sequences - $groupHash = is_object($group) ? spl_object_hash($group) : $group; - - if ($context->isGroupValidated($node->cacheKey, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) - unset($node->groups[$key]); - - continue; - } - - $context->markGroupAsValidated($node->cacheKey, $groupHash); - - // Replace the "Default" group by the group sequence defined - // for the class, if applicable - // This is done after checking the cache, so that - // spl_object_hash() isn't called for this sequence and - // "Default" is used instead in the cache. This is useful - // if the getters below return different group sequences in - // every call. - if (Constraint::DEFAULT_GROUP === $group) { - if ($node->metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $group = $node->metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - } elseif ($node->metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $node->value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - - if (!$group instanceof GroupSequence) { - $group = new GroupSequence($group); - } - } - } - } - - if ($group instanceof GroupSequence) { - // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $cascadedGroup, $context); - - // Skip the group sequence when validating successor nodes - unset($node->groups[$key]); - - continue; - } - - // Validate normal group - $this->validateInGroup($node, $group, $context); - } - - return true; - } - - /** - * Validates a node's value in each group of a group sequence. - * - * If any of the groups' constraints generates a violation, subsequent - * groups are not validated anymore. - * - * @param Node $node The validated node - * @param GroupSequence $groupSequence The group sequence - * @param ExecutionContextInterface $context The execution context - */ - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) - { - $violationCount = count($context->getViolations()); - $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; - - foreach ($groupSequence->groups as $groupInSequence) { - $node = clone $node; - $node->groups = array($groupInSequence); - $node->cascadedGroups = $cascadedGroups; - - $this->nodeTraverser->traverse(array($node), $context); - - // Abort sequence validation if a violation was generated - if (count($context->getViolations()) > $violationCount) { - break; - } - } - } - - /** - * Validates a node's value against all constraints in the given group. - * - * @param Node $node The validated node - * @param string $group The group to validate - * @param ExecutionContextInterface $context The execution context - * @param string $objectHash The hash of the node's - * object (if any) - * - * @throws \Exception - */ - private function validateInGroup(Node $node, $group, ExecutionContextInterface $context) - { - $context->setGroup($group); - - foreach ($node->metadata->findConstraints($group) as $constraint) { - // Prevent duplicate validation of constraints, in the case - // that constraints belong to multiple validated groups - if (null !== $node->cacheKey) { - $constraintHash = spl_object_hash($constraint); - - if ($context->isConstraintValidated($node->cacheKey, $constraintHash)) { - continue; - } - - $context->markConstraintAsValidated($node->cacheKey, $constraintHash); - } - - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($context); - $validator->validate($node->value, $constraint); - } - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php deleted file mode 100644 index ec05923f1bd5e..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ /dev/null @@ -1,59 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; - -/** - * A node visitor invoked by the node traverser. - * - * At the beginning of the traversal, the method {@link beforeTraversal()} is - * called. For each traversed node, the method {@link visit()} is called. At - * last, the method {@link afterTraversal()} is called when the traversal is - * complete. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see \Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface - */ -interface NodeVisitorInterface -{ - /** - * Called at the beginning of a traversal. - * - * @param Node[] $nodes A list of Node instances - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Whether to continue the traversal - */ - public function beforeTraversal($nodes, ExecutionContextInterface $context); - - /** - * Called at the end of a traversal. - * - * @param Node[] $nodes A list of Node instances - * @param ExecutionContextInterface $context The execution context - */ - public function afterTraversal($nodes, ExecutionContextInterface $context); - - /** - * Called for each node during a traversal. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Whether to traverse the node's successor nodes - */ - public function visit(Node $node, ExecutionContextInterface $context); -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php deleted file mode 100644 index 18fe19a955d71..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php +++ /dev/null @@ -1,83 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\ObjectInitializerInterface; - -/** - * Initializes the objects of all class nodes. - * - * You have to pass at least one instance of {@link ObjectInitializerInterface} - * to the constructor of this visitor. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class ObjectInitializationVisitor extends AbstractVisitor -{ - /** - * @var ObjectInitializerInterface[] - */ - private $initializers; - - /** - * Creates a new visitor. - * - * @param ObjectInitializerInterface[] $initializers The object initializers - * - * @throws InvalidArgumentException - */ - public function __construct(array $initializers) - { - foreach ($initializers as $initializer) { - if (!$initializer instanceof ObjectInitializerInterface) { - throw new InvalidArgumentException(sprintf( - 'Validator initializers must implement '. - '"Symfony\Component\Validator\ObjectInitializerInterface". '. - 'Got: "%s"', - is_object($initializer) ? get_class($initializer) : gettype($initializer) - )); - } - } - - // If no initializer is present, this visitor should not even be created - if (0 === count($initializers)) { - throw new InvalidArgumentException('Please pass at least one initializer.'); - } - - $this->initializers = $initializers; - } - - /** - * Calls the {@link ObjectInitializerInterface::initialize()} method for - * the object of each class node. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Always returns true - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - if ($node instanceof ClassNode) { - foreach ($this->initializers as $initializer) { - $initializer->initialize($node->value); - } - } - - return true; - } -} diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php deleted file mode 100644 index c79f4c838f60c..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php +++ /dev/null @@ -1,31 +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\Node; - -use Symfony\Component\Validator\Node\ClassNode; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class ClassNodeTest extends \PHPUnit_Framework_TestCase -{ - /** - * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException - */ - public function testConstructorExpectsObject() - { - $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); - - new ClassNode('foobar', null, $metadata, '', array(), array()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php deleted file mode 100644 index 09e26bcaf9e11..0000000000000 --- a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php +++ /dev/null @@ -1,91 +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\NodeTraverser; - -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\Node\GenericNode; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class NonRecursiveNodeTraverserTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var FakeMetadataFactory - */ - private $metadataFactory; - - /** - * @var NonRecursiveNodeTraverser - */ - private $traverser; - - protected function setUp() - { - $this->metadataFactory = new FakeMetadataFactory(); - $this->traverser = new NonRecursiveNodeTraverser($this->metadataFactory); - } - - public function testVisitorsMayPreventTraversal() - { - $nodes = array(new GenericNode('value', null, new GenericMetadata(), '', array('Default'))); - $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); - - $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); - $visitor2 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); - $visitor3 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); - - $visitor1->expects($this->once()) - ->method('beforeTraversal') - ->with($nodes, $context); - - // abort traversal - $visitor2->expects($this->once()) - ->method('beforeTraversal') - ->with($nodes, $context) - ->will($this->returnValue(false)); - - // never called - $visitor3->expects($this->never()) - ->method('beforeTraversal'); - - $visitor1->expects($this->never()) - ->method('visit'); - $visitor2->expects($this->never()) - ->method('visit'); - $visitor2->expects($this->never()) - ->method('visit'); - - // called in order to clean up - $visitor1->expects($this->once()) - ->method('afterTraversal') - ->with($nodes, $context); - - // abort traversal - $visitor2->expects($this->once()) - ->method('afterTraversal') - ->with($nodes, $context); - - // never called, because beforeTraversal() wasn't called either - $visitor3->expects($this->never()) - ->method('afterTraversal'); - - $this->traverser->addVisitor($visitor1); - $this->traverser->addVisitor($visitor2); - $this->traverser->addVisitor($visitor3); - - $this->traverser->traverse($nodes, $context); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php deleted file mode 100644 index 5c76a173a4c53..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.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\Validator; - -use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\ExecutionContextFactory; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\Validator\TraversingValidator; - -class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest -{ - protected function createValidator(MetadataFactoryInterface $metadataFactory) - { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); - $validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); - - $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory())); - - return $validator; - } -} diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index 5cd1198654697..9228d4564cf24 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -130,7 +130,7 @@ public function testSetApiVersion24() public function testSetApiVersion25() { $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_5)); - $this->assertInstanceOf('Symfony\Component\Validator\Validator\TraversingValidator', $this->builder->getValidator()); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\RecursiveValidator', $this->builder->getValidator()); } public function testSetApiVersion24And25() diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php deleted file mode 100644 index bd749eeb9757f..0000000000000 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ /dev/null @@ -1,242 +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\Validator; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\RuntimeException; -use Symfony\Component\Validator\Exception\UnsupportedMetadataException; -use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\GenericNode; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; -use Symfony\Component\Validator\Util\PropertyPath; - -/** - * Default implementation of {@link ContextualValidatorInterface}. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class TraversingContextualValidator implements ContextualValidatorInterface -{ - /** - * @var ExecutionContextInterface - */ - private $context; - - /** - * @var NodeTraverserInterface - */ - private $nodeTraverser; - - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - - /** - * Creates a validator for the given context. - * - * @param ExecutionContextInterface $context The execution context - * @param NodeTraverserInterface $nodeTraverser The node traverser - * @param MetadataFactoryInterface $metadataFactory The factory for fetching - * the metadata of validated - * objects - */ - public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) - { - $this->context = $context; - $this->defaultPropertyPath = $context->getPropertyPath(); - $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); - $this->nodeTraverser = $nodeTraverser; - $this->metadataFactory = $metadataFactory; - } - - /** - * {@inheritdoc} - */ - public function atPath($path) - { - $this->defaultPropertyPath = $this->context->getPropertyPath($path); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function validate($value, $constraints = null, $groups = null) - { - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - if (null !== $constraints) { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - - $node = new GenericNode( - $value, - is_object($value) ? spl_object_hash($value) : null, - $metadata, - $this->defaultPropertyPath, - $groups - ); - } elseif (is_array($value) || $value instanceof \Traversable && !$this->metadataFactory->hasMetadataFor($value)) { - $node = new CollectionNode( - $value, - $this->defaultPropertyPath, - $groups - ); - } elseif (is_object($value)) { - $metadata = $this->metadataFactory->getMetadataFor($value); - - if (!$metadata instanceof ClassMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($metadata) ? get_class($metadata) : gettype($metadata) - )); - } - - $node = new ClassNode( - $value, - spl_object_hash($value), - $metadata, - $this->defaultPropertyPath, - $groups - ); - } else { - throw new RuntimeException(sprintf( - 'Cannot validate values of type "%s" automatically. Please '. - 'provide a constraint.', - gettype($value) - )); - } - - $this->nodeTraverser->traverse(array($node), $this->context); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function validateProperty($object, $propertyName, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - // Cannot be UnsupportedMetadataException because of BC with - // Symfony < 2.5 - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($object); - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($object); - - $nodes[] = new PropertyNode( - $propertyValue, - $cacheKey.':'.$propertyName, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups - ); - } - - $this->nodeTraverser->traverse($nodes, $this->context); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - // Cannot be UnsupportedMetadataException because of BC with - // Symfony < 2.5 - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($object); - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $nodes[] = new PropertyNode( - $value, - $cacheKey.':'.$propertyName, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups, - $groups - ); - } - - $this->nodeTraverser->traverse($nodes, $this->context); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getViolations() - { - return $this->context->getViolations(); - } - - /** - * Normalizes the given group or list of groups to an array. - * - * @param mixed $groups The groups to normalize - * - * @return array A group array - */ - protected function normalizeGroups($groups) - { - if (is_array($groups)) { - return $groups; - } - - return array($groups); - } -} diff --git a/src/Symfony/Component/Validator/Validator/TraversingValidator.php b/src/Symfony/Component/Validator/Validator/TraversingValidator.php deleted file mode 100644 index 8fe07f630fb1f..0000000000000 --- a/src/Symfony/Component/Validator/Validator/TraversingValidator.php +++ /dev/null @@ -1,128 +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\Validator; - -use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; - -/** - * Default implementation of {@link ValidatorInterface}. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class TraversingValidator implements ValidatorInterface -{ - /** - * @var ExecutionContextFactoryInterface - */ - protected $contextFactory; - - /** - * @var NodeTraverserInterface - */ - protected $nodeTraverser; - - /** - * @var MetadataFactoryInterface - */ - protected $metadataFactory; - - /** - * Creates a new validator. - * - * @param ExecutionContextFactoryInterface $contextFactory The factory for - * creating new contexts - * @param NodeTraverserInterface $nodeTraverser The node traverser - * @param MetadataFactoryInterface $metadataFactory The factory for - * fetching the metadata - * of validated objects - */ - public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) - { - $this->contextFactory = $contextFactory; - $this->nodeTraverser = $nodeTraverser; - $this->metadataFactory = $metadataFactory; - } - - /** - * {@inheritdoc} - */ - public function startContext($root = null) - { - return new TraversingContextualValidator( - $this->contextFactory->createContext($this, $root), - $this->nodeTraverser, - $this->metadataFactory - ); - } - - /** - * {@inheritdoc} - */ - public function inContext(ExecutionContextInterface $context) - { - return new TraversingContextualValidator( - $context, - $this->nodeTraverser, - $this->metadataFactory - ); - } - - /** - * {@inheritdoc} - */ - public function getMetadataFor($object) - { - return $this->metadataFactory->getMetadataFor($object); - } - - /** - * {@inheritdoc} - */ - public function hasMetadataFor($object) - { - return $this->metadataFactory->hasMetadataFor($object); - } - - /** - * {@inheritdoc} - */ - public function validate($value, $constraints = null, $groups = null) - { - return $this->startContext($value) - ->validate($value, $constraints, $groups) - ->getViolations(); - } - - /** - * {@inheritdoc} - */ - public function validateProperty($object, $propertyName, $groups = null) - { - return $this->startContext($object) - ->validateProperty($object, $propertyName, $groups) - ->getViolations(); - } - - /** - * {@inheritdoc} - */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) - { - return $this->startContext($object) - ->validatePropertyValue($object, $propertyName, $value, $groups) - ->getViolations(); - } -} diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 2d66fa9f6af5b..8250ea6684724 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -35,6 +35,7 @@ use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator\LegacyValidator; +use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator as ValidatorV24; @@ -416,13 +417,7 @@ public function getValidator() $contextFactory = new LegacyExecutionContextFactory($metadataFactory, $translator, $this->translationDomain); if (Validation::API_VERSION_2_5 === $apiVersion) { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - if (count($this->initializers) > 0) { - $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); - } - $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory)); - - return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); + return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory); } return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory); From 166d71a7de2bdffda3b2185c30c4841dcaf0f013 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 10:16:30 +0100 Subject: [PATCH 086/323] [Validator] Removed unused property --- .../Component/Validator/Context/ExecutionContext.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 75b7e2c3bf6bc..6ee58d8403a29 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -106,13 +106,6 @@ class ExecutionContext implements ExecutionContextInterface */ private $validatedConstraints = array(); - /** - * Stores which property constraint has been validated for which property. - * - * @var array - */ - private $validatedPropertyConstraints = array(); - /** * Creates a new execution context. * From 7bc952de55a5b936afbffd2f8eda5d34386e6e55 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 11:36:55 +0100 Subject: [PATCH 087/323] [Validator] Improved inline documentation of RecursiveContextualValidator --- .../Validator/Context/ExecutionContext.php | 10 +- .../Context/ExecutionContextInterface.php | 3 +- .../RecursiveContextualValidator.php | 529 ++++++++++-------- 3 files changed, 319 insertions(+), 223 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 6ee58d8403a29..c345e4115fd5b 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -71,6 +71,13 @@ class ExecutionContext implements ExecutionContextInterface */ private $value; + /** + * The currently validated object. + * + * @var object|null + */ + private $object; + /** * The property path leading to the current value. * @@ -132,9 +139,10 @@ public function __construct(ValidatorInterface $validator, $root, TranslatorInte /** * {@inheritdoc} */ - public function setNode($value, MetadataInterface $metadata = null, $propertyPath) + public function setNode($value, $object, MetadataInterface $metadata, $propertyPath) { $this->value = $value; + $this->object = $object; $this->metadata = $metadata; $this->propertyPath = (string) $propertyPath; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 2e778fbec815f..b0b8c2b97b201 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -103,13 +103,14 @@ public function getValidator(); * Sets the currently validated value. * * @param mixed $value The validated value + * @param object|null $object The currently validated object * @param MetadataInterface $metadata The validation metadata * @param string $propertyPath The property path to the current value * * @internal Used by the validator engine. Should not be called by user * code. */ - public function setNode($value, MetadataInterface $metadata = null, $propertyPath); + public function setNode($value, $object, MetadataInterface $metadata, $propertyPath); /** * Sets the currently validated group. diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 0e083c05cf8fe..0da7c7cf51104 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -27,10 +27,6 @@ use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Util\PropertyPath; /** @@ -92,7 +88,11 @@ public function validate($value, $constraints = null, $groups = null) { $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + // If explicit constraints are passed, validate the value against + // those constraints if (null !== $constraints) { + // You can pass a single constraint or an array of constraints + // Make sure to deal with an array in the rest of the code if (!is_array($constraints)) { $constraints = array($constraints); } @@ -102,6 +102,7 @@ public function validate($value, $constraints = null, $groups = null) $this->validateGenericNode( $value, + null, is_object($value) ? spl_object_hash($value) : null, $metadata, $this->defaultPropertyPath, @@ -114,8 +115,10 @@ public function validate($value, $constraints = null, $groups = null) return $this; } + // If an object is passed without explicit constraints, validate that + // object against the constraints defined for the object's class if (is_object($value)) { - $this->cascadeObject( + $this->validateObject( $value, $this->defaultPropertyPath, $groups, @@ -126,12 +129,14 @@ public function validate($value, $constraints = null, $groups = null) return $this; } + // If an array is passed without explicit constraints, validate each + // object in the array if (is_array($value)) { - $this->cascadeCollection( + $this->validateEachObjectIn( $value, $this->defaultPropertyPath, $groups, - TraversalStrategy::IMPLICIT, + true, $this->context ); @@ -148,9 +153,9 @@ public function validate($value, $constraints = null, $groups = null) /** * {@inheritdoc} */ - public function validateProperty($container, $propertyName, $groups = null) + public function validateProperty($object, $propertyName, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($container); + $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -165,13 +170,14 @@ public function validateProperty($container, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($container); + $cacheKey = spl_object_hash($object); foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($container); + $propertyValue = $propertyMetadata->getPropertyValue($object); $this->validateGenericNode( $propertyValue, + $object, $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), @@ -188,9 +194,9 @@ public function validateProperty($container, $propertyName, $groups = null) /** * {@inheritdoc} */ - public function validatePropertyValue($container, $propertyName, $value, $groups = null) + public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($container); + $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -205,11 +211,12 @@ public function validatePropertyValue($container, $propertyName, $value, $groups $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($container); + $cacheKey = spl_object_hash($object); foreach ($propertyMetadatas as $propertyMetadata) { $this->validateGenericNode( $value, + $object, $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), @@ -246,53 +253,197 @@ protected function normalizeGroups($groups) return array($groups); } - /** - * Traverses a class node. + * Validates an object against the constraints defined for its class. + * + * If no metadata is available for the class, but the class is an instance + * of {@link \Traversable} and the selected traversal strategy allows + * traversal, the object will be iterated and each nested object will be + * validated instead. + * + * @param object $object The object to cascade + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param integer $traversalStrategy The strategy for traversing the + * cascaded object + * @param ExecutionContextInterface $context The current execution context * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, a property - * node is put on the node stack for each constrained property of the class. - * At last, if the class is traversable and should be traversed according - * to the selected traversal strategy, a new collection node is put on the - * stack. + * @throws NoSuchMetadataException If the object has no associated metadata + * and does not implement {@link \Traversable} + * or if traversal is disabled via the + * $traversalStrategy argument + * @throws UnsupportedMetadataException If the metadata returned by the + * metadata factory does not implement + * {@link ClassMetadataInterface} + */ + private function validateObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $this->validateClassNode( + $object, + spl_object_hash($object), + $classMetadata, + $propertyPath, + $groups, + null, + $traversalStrategy, + $context + ); + } catch (NoSuchMetadataException $e) { + // Rethrow if not Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + // Rethrow unless IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + throw $e; + } + + $this->validateEachObjectIn( + $object, + $propertyPath, + $groups, + $traversalStrategy & TraversalStrategy::STOP_RECURSION, + $context + ); + } + } + + /** + * Validates each object in a collection against the constraints defined + * for their classes. * - * @param ClassNode $node The class node - * @param ExecutionContextInterface $context The current execution context + * If the parameter $recursive is set to true, nested {@link \Traversable} + * objects are iterated as well. Nested arrays are always iterated, + * regardless of the value of $recursive. * - * @throws UnsupportedMetadataException If a property metadata does not - * implement {@link PropertyMetadataInterface} + * @param array|\Traversable $collection The collection + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param Boolean $stopRecursion Whether to disable + * recursive iteration. For + * backwards compatibility + * with Symfony < 2.5. + * @param ExecutionContextInterface $context The current execution context * * @see ClassNode - * @see PropertyNode * @see CollectionNode - * @see TraversalStrategy */ - private function validateClassNode($value, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateEachObjectIn($collection, $propertyPath, array $groups, $stopRecursion, ExecutionContextInterface $context) { - $context->setNode($value, $metadata, $propertyPath); + if ($stopRecursion) { + $traversalStrategy = TraversalStrategy::NONE; + } else { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } - // if group (=[,G3,G4]) contains group sequence (=) - // then call traverse() with each entry of the group sequence and abort - // if necessary (G1, G2) - // finally call traverse() with remaining entries ([G3,G4]) or - // simply continue traversal (if possible) + foreach ($collection as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->validateEachObjectIn( + $value, + $propertyPath.'['.$key.']', + $groups, + $stopRecursion, + $context + ); - foreach ($groups as $key => $group) { - $cascadedGroup = null; + continue; + } - // Even if we remove the following clause, the constraints on an - // object won't be validated again due to the measures taken in - // validateNodeForGroup(). - // The following shortcut, however, prevents validatedNodeForGroup() - // from being called at all and enhances performance a bit. + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->validateObject( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); + } + } + } + + /** + * Validates a class node. + * + * A class node is a combination of an object with a {@link ClassMetadataInterface} + * instance. Each class node (conceptionally) has zero or more succeeding + * property nodes: + * + * (Article:class node) + * \ + * ($title:property node) + * + * This method validates the passed objects against all constraints defined + * at class level. It furthermore triggers the validation of each of the + * class' properties against the constraints for that property. + * + * If the selected traversal strategy allows traversal, the object is + * iterated and each nested object is validated against its own constraints. + * The object is not traversed if traversal is disabled in the class + * metadata. + * + * If the passed groups contain the group "Default", the validator will + * check whether the "Default" group has been replaced by a group sequence + * in the class metadata. If this is the case, the group sequence is + * validated instead. + * + * @param object $object The validated object + * @param string $cacheKey The key for caching + * the validated object + * @param ClassMetadataInterface $metadata The class metadata of + * the object + * @param string $propertyPath The property path leading + * to the object + * @param string[] $groups The groups in which the + * object should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should + * be validated + * @param integer $traversalStrategy The strategy used for + * traversing the object + * @param ExecutionContextInterface $context The current execution context + * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * @throws ConstraintDefinitionException If traversal was enabled but the + * object does not implement + * {@link \Traversable} + * + * @see TraversalStrategy + */ + private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + { + $context->setNode($object, $object, $metadata, $propertyPath); + + foreach ($groups as $key => $group) { + // If the "Default" group is replaced by a group sequence, remember + // to cascade the "Default" group when traversing the group + // sequence + $defaultOverridden = false; // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; if ($context->isGroupValidated($cacheKey, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) + // Skip this group when validating the properties and when + // traversing the object unset($groups[$key]); continue; @@ -301,7 +452,7 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me $context->markGroupAsValidated($cacheKey, $groupHash); // Replace the "Default" group by the group sequence defined - // for the class, if applicable + // for the class, if applicable. // This is done after checking the cache, so that // spl_object_hash() isn't called for this sequence and // "Default" is used instead in the cache. This is useful @@ -311,13 +462,13 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me if ($metadata->hasGroupSequence()) { // The group sequence is statically defined for the class $group = $metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; + $defaultOverridden = true; } elseif ($metadata->isGroupSequenceProvider()) { // The group sequence is dynamically obtained from the validated // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */ + $group = $object->getGroupSequence(); + $defaultOverridden = true; if (!$group instanceof GroupSequence) { $group = new GroupSequence($group); @@ -325,23 +476,43 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me } } + // If the groups (=[,G3,G4]) contain a group sequence + // (=), then call validateClassNode() with each entry of the + // group sequence and abort if necessary (G1, G2) if ($group instanceof GroupSequence) { - $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + $this->stepThroughGroupSequence( + $object, + $object, + $cacheKey, + $metadata, + $propertyPath, + $traversalStrategy, + $group, + $defaultOverridden ? Constraint::DEFAULT_GROUP : null, + $context + ); - // Skip the group sequence when validating successor nodes + // Skip the group sequence when validating properties, because + // stepThroughGroupSequence() already validates the properties unset($groups[$key]); continue; } - $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + $this->validateInGroup($object, $cacheKey, $metadata, $group, $context); } + // If no more groups should be validated for the property nodes, + // we can safely quit if (0 === count($groups)) { return; } + // Validate all properties against their constraints foreach ($metadata->getConstrainedProperties() as $propertyName) { + // If constraints are defined both on the getter of a property as + // well as on the property itself, then getPropertyMetadata() + // returns two metadata objects, not just one foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { if (!$propertyMetadata instanceof PropertyMetadataInterface) { throw new UnsupportedMetadataException(sprintf( @@ -352,10 +523,11 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me )); } - $propertyValue = $propertyMetadata->getPropertyValue($value); + $propertyValue = $propertyMetadata->getPropertyValue($object); $this->validateGenericNode( $propertyValue, + $object, $cacheKey.':'.$propertyName, $propertyMetadata, $propertyPath @@ -383,56 +555,85 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me } // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) { return; } // If TRAVERSE, fail if we have no Traversable - if (!$value instanceof \Traversable) { + if (!$object instanceof \Traversable) { // Must throw a ConstraintDefinitionException for backwards // compatibility reasons with Symfony < 2.5 throw new ConstraintDefinitionException(sprintf( 'Traversal was enabled for "%s", but this class '. 'does not implement "\Traversable".', - get_class($value) + get_class($object) )); } - $this->cascadeCollection( - $value, + $this->validateEachObjectIn( + $object, $propertyPath, $groups, - $traversalStrategy, + $traversalStrategy & TraversalStrategy::STOP_RECURSION, $context ); } /** - * Traverses a node that is neither a class nor a collection node. + * Validates a node that is not a class node. * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: + * Currently, two such node types exist: * - * - if the node contains an object with associated class metadata, a new - * class node is put on the stack; - * - if the node contains a traversable object without associated class - * metadata and traversal is enabled according to the selected traversal - * strategy, a collection node is put on the stack; - * - if the node contains an array, a collection node is put on the stack. + * - property nodes, which consist of the value of an object's + * property together with a {@link PropertyMetadataInterface} instance + * - generic nodes, which consist of a value and some arbitrary + * constraints defined in a {@link MetadataInterface} container * - * @param Node $node The node - * @param ExecutionContextInterface $context The current execution context + * In both cases, the value is validated against all constraints defined + * in the passed metadata object. Then, if the value is an instance of + * {@link \Traversable} and the selected traversal strategy permits it, + * the value is traversed and each nested object validated against its own + * constraints. Arrays are always traversed. + * + * @param mixed $value The validated value + * @param object|null $object The current object + * @param string $cacheKey The key for caching + * the validated value + * @param MetadataInterface $metadata The metadata of the + * value + * @param string $propertyPath The property path leading + * to the value + * @param string[] $groups The groups in which the + * value should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should + * be validated + * @param integer $traversalStrategy The strategy used for + * traversing the value + * @param ExecutionContextInterface $context The current execution context + * + * @see TraversalStrategy */ - private function validateGenericNode($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $context->setNode($value, $metadata, $propertyPath); + $context->setNode($value, $object, $metadata, $propertyPath); foreach ($groups as $key => $group) { if ($group instanceof GroupSequence) { - $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, null, $context); + $this->stepThroughGroupSequence( + $value, + $object, + $cacheKey, + $metadata, + $propertyPath, + $traversalStrategy, + $group, + null, + $context + ); - // Skip the group sequence when validating successor nodes + // Skip the group sequence when cascading, as the cascading + // logic is already done in stepThroughGroupSequence() unset($groups[$key]); continue; @@ -464,8 +665,9 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences + // The $cascadedGroups property is set, if the "Default" group is + // overridden by a group sequence + // See validateClassNode() $cascadedGroups = count($cascadedGroups) > 0 ? $cascadedGroups : $groups; @@ -474,11 +676,11 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->cascadeCollection( + $this->validateEachObjectIn( $value, $propertyPath, $cascadedGroups, - $traversalStrategy, + $traversalStrategy & TraversalStrategy::STOP_RECURSION, $context ); @@ -488,7 +690,7 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad // If the value is a scalar, pass it anyway, because we want // a NoSuchMetadataException to be thrown in that case // (BC with Symfony < 2.5) - $this->cascadeObject( + $this->validateObject( $value, $propertyPath, $cascadedGroups, @@ -498,151 +700,36 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad // Currently, the traversal strategy can only be TRAVERSE for a // generic node if the cascading strategy is CASCADE. Thus, traversable - // objects will always be handled within cascadeObject() and there's + // objects will always be handled within validateObject() and there's // nothing more to do here. // see GenericMetadata::addConstraint() } /** - * Executes the cascading logic for an object. + * Sequentially validates a node's value in each group of a group sequence. * - * If class metadata is available for the object, a class node is put on - * the node stack. Otherwise, if the selected traversal strategy allows - * traversal of the object, a new collection node is put on the stack. - * Otherwise, an exception is thrown. - * - * @param object $container The object to cascade - * @param string $propertyPath The current property path - * @param string[] $groups The validated groups - * @param integer $traversalStrategy The strategy for traversing the - * cascaded object - * @param ExecutionContextInterface $context The current execution context + * If any of the constraints generates a violation, subsequent groups in the + * group sequence are skipped. * - * @throws NoSuchMetadataException If the object has no associated metadata - * and does not implement {@link \Traversable} - * or if traversal is disabled via the - * $traversalStrategy argument - * @throws UnsupportedMetadataException If the metadata returned by the - * metadata factory does not implement - * {@link ClassMetadataInterface} + * @param mixed $value The validated value + * @param object|null $object The current object + * @param string $cacheKey The key for caching + * the validated value + * @param MetadataInterface $metadata The metadata of the + * value + * @param string $propertyPath The property path leading + * to the value + * @param integer $traversalStrategy The strategy used for + * traversing the value + * @param GroupSequence $groupSequence The group sequence + * @param string[]|null $cascadedGroup The group that should + * be passed to cascaded + * objects instead of + * the group sequence + * @param ExecutionContextInterface $context The execution context */ - private function cascadeObject($container, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($container); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The metadata factory should return instances of '. - '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $this->validateClassNode( - $container, - spl_object_hash($container), - $classMetadata, - $propertyPath, - $groups, - null, - $traversalStrategy, - $context - ); - } catch (NoSuchMetadataException $e) { - // Rethrow if not Traversable - if (!$container instanceof \Traversable) { - throw $e; - } - - // Rethrow unless IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - throw $e; - } - - $this->cascadeCollection( - $container, - $propertyPath, - $groups, - $traversalStrategy, - $context - ); - } - } - - /** - * Traverses a collection node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: - * - * - for each object in the collection with associated class metadata, a - * new class node is put on the stack; - * - if an object has no associated class metadata, but is traversable, and - * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for - * collection node, a new collection node is put on the stack for that - * object; - * - for each array in the collection, a new collection node is put on the - * stack. - * - * @param CollectionNode $node The collection node - * @param ExecutionContextInterface $context The current execution context - * - * @see ClassNode - * @see CollectionNode - */ - private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { - $traversalStrategy = TraversalStrategy::NONE; - } else { - $traversalStrategy = TraversalStrategy::IMPLICIT; - } - - foreach ($collection as $key => $value) { - if (is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $this->cascadeCollection( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - } - } - } - - /** - * Validates a node's value in each group of a group sequence. - * - * If any of the groups' constraints generates a violation, subsequent - * groups are not validated anymore. - * - * @param Node $node The validated node - * @param GroupSequence $groupSequence The group sequence - * @param ExecutionContextInterface $context The execution context - */ - private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; @@ -664,6 +751,7 @@ private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $ } else { $this->validateGenericNode( $value, + $object, $cacheKey, $metadata, $propertyPath, @@ -684,13 +772,12 @@ private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $ /** * Validates a node's value against all constraints in the given group. * - * @param Node $node The validated node + * @param mixed $value The validated value + * @param string $cacheKey The key for caching the + * validated value + * @param MetadataInterface $metadata The metadata of the value * @param string $group The group to validate * @param ExecutionContextInterface $context The execution context - * @param string $containerHash The hash of the node's - * object (if any) - * - * @throws \Exception */ private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context) { From 1b111d0e831111c3499ef7975da9f18f0c8b5206 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 11:42:12 +0100 Subject: [PATCH 088/323] [Validator] Fixed typos pointed out by @cordoval --- src/Symfony/Component/Validator/ConstraintViolation.php | 2 +- .../Component/Validator/Constraints/GroupSequence.php | 8 ++++---- .../Validator/Mapping/ClassMetadataInterface.php | 2 -- .../Component/Validator/Mapping/GenericMetadata.php | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 41f57650a13b8..fa8d70b9543cc 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -72,7 +72,7 @@ class ConstraintViolation implements ConstraintViolationInterface * @param mixed $invalidValue The invalid value that caused this * violation * @param integer|null $plural The number for determining the plural - * form when translation the message + * form when translating the message * @param mixed $code The error code of the violation */ public function __construct($message, $messageTemplate, array $parameters, $root, $propertyPath, $invalidValue, $plural = null, $code = null) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index af3b86c1c6fb7..805aa1b16b56d 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -19,14 +19,14 @@ * When validating a group sequence, each group will only be validated if all * of the previous groups in the sequence succeeded. For example: * - * $validator->validate($address, new Valid(), new GroupSequence('Basic', 'Strict')); + * $validator->validate($address, null, new GroupSequence('Basic', 'Strict')); * * In the first step, all constraints that belong to the group "Basic" will be * validated. If none of the constraints fail, the validator will then validate * the constraints in group "Strict". This is useful, for example, if "Strict" * contains expensive checks that require a lot of CPU or slow, external * services. You usually don't want to run expensive checks if any of the cheap - * checks fails. + * checks fail. * * When adding metadata to a class, you can override the "Default" group of * that class with a group sequence: @@ -42,12 +42,12 @@ * Whenever you validate that object in the "Default" group, the group sequence * will be validated: * - * $validator->validate($address, new Valid()); + * $validator->validate($address); * * If you want to execute the constraints of the "Default" group for a class * with an overridden default group, pass the class name as group name instead: * - * $validator->validate($address, new Valid(), "Address") + * $validator->validate($address, null, "Address") * * @Annotation * diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index 0e0d2448d7c59..332f5fa1c86cb 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -14,8 +14,6 @@ use Symfony\Component\Validator\ClassBasedInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface; -; - /** * Stores all metadata needed for validating objects of specific class. * diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 01b3d5a403f04..33b40fbd369c6 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -26,7 +26,7 @@ * @since 2.5 * @author Bernhard Schussek */ -class GenericMetadata implements MetadataInterface +class GenericMetadata implements MetadataInterface { /** * @var Constraint[] From 0946dbe7a0640f374b491e6ceadcb344acbdae15 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 12:31:30 +0100 Subject: [PATCH 089/323] [Validator] Adapted CHANGELOG --- src/Symfony/Component/Validator/CHANGELOG.md | 51 +++++- .../Validator/Context/ExecutionContext.php | 4 +- .../Component/Validator/ExecutionContext.php | 16 +- .../Validator/ExecutionContextInterface.php | 26 +-- .../Mapping/BlackholeMetadataFactory.php | 28 +-- .../Mapping/ClassMetadataFactory.php | 150 +--------------- .../Factory/BlackHoleMetadataFactory.php | 40 +++++ .../Mapping/Factory/LazyMetadataFactory.php | 165 ++++++++++++++++++ .../Factory/MetadataFactoryInterface.php | 24 +++ .../Validator/MetadataFactoryInterface.php | 3 + .../Component/Validator/ValidatorBuilder.php | 4 - 11 files changed, 316 insertions(+), 195 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php create mode 100644 src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php create mode 100644 src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 005526215beae..57d6b17bf562d 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -7,8 +7,57 @@ CHANGELOG * deprecated `ApcCache` in favor of `DoctrineCache` * added `DoctrineCache` to adapt any Doctrine cache * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` - * changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array + * [BC BREAK] changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array * `Callback` can now be put onto properties (useful when you pass a closure to the constraint) + * deprecated `ClassBasedInterface` + * deprecated `MetadataInterface` + * deprecated `PropertyMetadataInterface` + * deprecated `PropertyMetadataContainerInterface` + * deprecated `Mapping\ElementMetadata` + * added `Mapping\MetadataInterface` + * added `Mapping\ClassMetadataInterface` + * added `Mapping\PropertyMetadataInterface` + * added `Mapping\GenericMetadata` + * added `Mapping\CascadingStrategy` + * added `Mapping\TraversalStrategy` + * deprecated `Mapping\ClassMetadata::accept()` + * deprecated `Mapping\MemberMetadata::accept()` + * removed array type hint of `Mapping\ClassMetadata::setGroupSequence()` + * deprecated `MetadataFactoryInterface` + * deprecated `Mapping\BlackholeMetadataFactory` + * deprecated `Mapping\ClassMetadataFactory` + * added `Mapping\Factory\MetadataFactoryInterface` + * added `Mapping\Factory\BlackHoleMetadataFactory` + * added `Mapping\Factory\LazyMetadataFactory` + * deprecated `ExecutionContextInterface` + * deprecated `ExecutionContext` + * deprecated `GlobalExecutionContextInterface` + * added `Context\ExecutionContextInterface` + * added `Context\ExecutionContext` + * added `Context\ExecutionContextFactoryInterface` + * added `Context\ExecutionContextFactory` + * deprecated `ValidatorInterface` + * deprecated `Validator` + * deprecated `ValidationVisitorInterface` + * deprecated `ValidationVisitor` + * added `Validator\ValidatorInterface` + * added `Validator\RecursiveValidator` + * added `Validator\ContextualValidatorInterface` + * added `Validator\RecursiveContextualValidator` + * added `Violation\ConstraintViolationBuilderInterface` + * added `Violation\ConstraintViolationBuilder` + * added `ConstraintViolation::getParameters()` + * added `ConstraintViolation::getPlural()` + * added `Constraints\Traverse` + * deprecated `$deep` property in `Constraints\Valid` + * added `ValidatorBuilderInterface::setApiVersion()` + * added `Validation::API_VERSION_2_4` + * added `Validation::API_VERSION_2_5` + * added `Exception\OutOfBoundsException` + * added `Exception\UnsupportedMetadataException` + * made `Exception\ValidatorException` extend `Exception\RuntimeException` + * added `Util\PropertyPath` + 2.4.0 ----- diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index c345e4115fd5b..beeef001e42c1 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -158,7 +158,7 @@ public function setGroup($group) /** * {@inheritdoc} */ - public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { // The parameters $invalidValue and following are ignored by the new // API, as they are not present in the new interface anymore. @@ -275,7 +275,7 @@ public function getPropertyPath($subPath = '') /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { throw new BadMethodCallException( 'addViolationAt() is not supported anymore as of Symfony 2.5. '. diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 6435bbf9d0568..5407744bb7093 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -90,13 +90,13 @@ public function __construct(GlobalExecutionContextInterface $globalContext, Tran /** * {@inheritdoc} */ - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $params = array(), $invalidValue = null, $plural = null, $code = null) { - if (null === $pluralization) { + if (null === $plural) { $translatedMessage = $this->translator->trans($message, $params, $this->translationDomain); } else { try { - $translatedMessage = $this->translator->transChoice($message, $pluralization, $params, $this->translationDomain); + $translatedMessage = $this->translator->transChoice($message, $plural, $params, $this->translationDomain); } catch (\InvalidArgumentException $e) { $translatedMessage = $this->translator->trans($message, $params, $this->translationDomain); } @@ -110,7 +110,7 @@ public function addViolation($message, array $params = array(), $invalidValue = $this->propertyPath, // check using func_num_args() to allow passing null values func_num_args() >= 3 ? $invalidValue : $this->value, - $pluralization, + $plural, $code )); } @@ -118,19 +118,19 @@ public function addViolation($message, array $params = array(), $invalidValue = /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { $this->globalContext->getViolations()->add(new ConstraintViolation( - null === $pluralization + null === $plural ? $this->translator->trans($message, $parameters, $this->translationDomain) - : $this->translator->transChoice($message, $pluralization, $parameters, $this->translationDomain), + : $this->translator->transChoice($message, $plural, $parameters, $this->translationDomain), $message, $parameters, $this->globalContext->getRoot(), $this->getPropertyPath($subPath), // check using func_num_args() to allow passing null values func_num_args() >= 4 ? $invalidValue : $this->value, - $pluralization, + $plural, $code )); } diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php index b89caa2deaee2..3705bc43102ac 100644 --- a/src/Symfony/Component/Validator/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -91,11 +91,11 @@ 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. + * @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 $plural The number to use to pluralize of the message + * @param integer|null $code The violation code * * @api * @@ -103,18 +103,18 @@ interface ExecutionContextInterface * deprecated since version 2.5 and will be removed in * Symfony 3.0. */ - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); + public function addViolation($message, array $params = array(), $invalidValue = null, $plural = 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 $parameters 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. + * @param string $subPath The relative property path for the violation + * @param string $message The error message + * @param array $parameters The parameters substituted in the error message + * @param mixed $invalidValue The invalid, validated value + * @param integer|null $plural The number to use to pluralize of the message + * @param integer|null $code The violation code * * @api * @@ -122,7 +122,7 @@ public function addViolation($message, array $params = array(), $invalidValue = * Use {@link Context\ExecutionContextInterface::buildViolation()} * instead. */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null); + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null); /** * Validates the given value within the scope of the current validation. diff --git a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php index 28eaa5f02def4..7913e15625499 100644 --- a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php @@ -11,32 +11,14 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\MetadataFactoryInterface; - /** - * Metadata factory that does not store metadata. - * - * This implementation is useful if you want to validate values against - * constraints only and you don't need to add constraints to classes and - * properties. + * Alias of {@link Factory\BlackHoleMetadataFactory}. * * @author Fabien Potencier + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Factory\BlackHoleMetadataFactory} instead. */ -class BlackholeMetadataFactory implements MetadataFactoryInterface +class BlackholeMetadataFactory extends \Symfony\Component\Validator\Mapping\Factory\BlackHoleMetadataFactory { - /** - * {@inheritdoc} - */ - public function getMetadataFor($value) - { - throw new \LogicException('This class does not support metadata.'); - } - - /** - * {@inheritdoc} - */ - public function hasMetadataFor($value) - { - return false; - } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 8c26b7acecaba..956b798ed7635 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -11,154 +11,16 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Mapping\Cache\CacheInterface; -use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Mapping\Factory\LazyMetadataFactory; /** - * Creates new {@link ClassMetadataInterface} instances. - * - * Whenever {@link getMetadataFor()} is called for the first time with a given - * class name or object of that class, a new metadata instance is created and - * returned. On subsequent requests for the same class, the same metadata - * instance will be returned. - * - * You can optionally pass a {@link LoaderInterface} instance to the constructor. - * Whenever a new metadata instance is created, it is passed to the loader, - * which can configure the metadata based on configuration loaded from the - * filesystem or a database. If you want to use multiple loaders, wrap them in a - * {@link Loader\LoaderChain}. - * - * You can also optionally pass a {@link CacheInterface} instance to the - * constructor. This cache will be used for persisting the generated metadata - * between multiple PHP requests. + * Alias of {@link LazyMetadataFactory}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link LazyMetadataFactory} instead. */ -class ClassMetadataFactory implements MetadataFactoryInterface +class ClassMetadataFactory extends LazyMetadataFactory { - /** - * The loader for loading the class metadata - * - * @var LoaderInterface - */ - protected $loader; - - /** - * The cache for caching class metadata - * - * @var CacheInterface - */ - protected $cache; - - /** - * The loaded metadata, indexed by class name - * - * @var ClassMetadata[] - */ - protected $loadedClasses = array(); - - /** - * Creates a new metadata factory. - * - * @param LoaderInterface|null $loader The loader for configuring new metadata - * @param CacheInterface|null $cache The cache for persisting metadata - * between multiple PHP requests - */ - public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null) - { - $this->loader = $loader; - $this->cache = $cache; - } - - /** - * Returns the metadata for the given class name or object. - * - * If the method was called with the same class name (or an object of that - * class) before, the same metadata instance is returned. - * - * If the factory was configured with a cache, this method will first look - * for an existing metadata instance in the cache. If an existing instance - * is found, it will be returned without further ado. - * - * Otherwise, a new metadata instance is created. If the factory was - * configured with a loader, the metadata is passed to the - * {@link LoaderInterface::loadClassMetadata()} method for further - * configuration. At last, the new object is returned. - * - * @param string|object $value A class name or an object - * - * @return MetadataInterface The metadata for the value - * - * @throws NoSuchMetadataException If no metadata exists for the given value - */ - public function getMetadataFor($value) - { - if (!is_object($value) && !is_string($value)) { - throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: %s', gettype($value))); - } - - $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); - - if (isset($this->loadedClasses[$class])) { - return $this->loadedClasses[$class]; - } - - if (null !== $this->cache && false !== ($this->loadedClasses[$class] = $this->cache->read($class))) { - return $this->loadedClasses[$class]; - } - - if (!class_exists($class) && !interface_exists($class)) { - throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class)); - } - - $metadata = new ClassMetadata($class); - - // Include constraints from the parent class - if ($parent = $metadata->getReflectionClass()->getParentClass()) { - $metadata->mergeConstraints($this->getMetadataFor($parent->name)); - } - - // Include constraints from all implemented interfaces - foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { - if ('Symfony\Component\Validator\GroupSequenceProviderInterface' === $interface->name) { - continue; - } - $metadata->mergeConstraints($this->getMetadataFor($interface->name)); - } - - if (null !== $this->loader) { - $this->loader->loadClassMetadata($metadata); - } - - if (null !== $this->cache) { - $this->cache->write($metadata); - } - - return $this->loadedClasses[$class] = $metadata; - } - - /** - * Returns whether the factory is able to return metadata for the given - * class name or object. - * - * @param string|object $value A class name or an object - * - * @return Boolean Whether metadata can be returned for that class - */ - 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; - } } diff --git a/src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php new file mode 100644 index 0000000000000..5b38d0c98a775 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.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\Mapping\Factory; + +/** + * Metadata factory that does not store metadata. + * + * This implementation is useful if you want to validate values against + * constraints only and you don't need to add constraints to classes and + * properties. + * + * @author Fabien Potencier + */ +class BlackHoleMetadataFactory implements MetadataFactoryInterface +{ + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + throw new \LogicException('This class does not support metadata.'); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + return false; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php new file mode 100644 index 0000000000000..ef069b61c1d70 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Factory; + +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Mapping\Cache\CacheInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * Creates new {@link ClassMetadataInterface} instances. + * + * Whenever {@link getMetadataFor()} is called for the first time with a given + * class name or object of that class, a new metadata instance is created and + * returned. On subsequent requests for the same class, the same metadata + * instance will be returned. + * + * You can optionally pass a {@link LoaderInterface} instance to the constructor. + * Whenever a new metadata instance is created, it is passed to the loader, + * which can configure the metadata based on configuration loaded from the + * filesystem or a database. If you want to use multiple loaders, wrap them in a + * {@link Loader\LoaderChain}. + * + * You can also optionally pass a {@link CacheInterface} instance to the + * constructor. This cache will be used for persisting the generated metadata + * between multiple PHP requests. + * + * @author Bernhard Schussek + */ +class LazyMetadataFactory implements MetadataFactoryInterface +{ + /** + * The loader for loading the class metadata + * + * @var LoaderInterface + */ + protected $loader; + + /** + * The cache for caching class metadata + * + * @var CacheInterface + */ + protected $cache; + + /** + * The loaded metadata, indexed by class name + * + * @var ClassMetadata[] + */ + protected $loadedClasses = array(); + + /** + * Creates a new metadata factory. + * + * @param LoaderInterface|null $loader The loader for configuring new metadata + * @param CacheInterface|null $cache The cache for persisting metadata + * between multiple PHP requests + */ + public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null) + { + $this->loader = $loader; + $this->cache = $cache; + } + + /** + * Returns the metadata for the given class name or object. + * + * If the method was called with the same class name (or an object of that + * class) before, the same metadata instance is returned. + * + * If the factory was configured with a cache, this method will first look + * for an existing metadata instance in the cache. If an existing instance + * is found, it will be returned without further ado. + * + * Otherwise, a new metadata instance is created. If the factory was + * configured with a loader, the metadata is passed to the + * {@link LoaderInterface::loadClassMetadata()} method for further + * configuration. At last, the new object is returned. + * + * @param string|object $value A class name or an object + * + * @return MetadataInterface The metadata for the value + * + * @throws NoSuchMetadataException If no metadata exists for the given value + */ + public function getMetadataFor($value) + { + if (!is_object($value) && !is_string($value)) { + throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: %s', gettype($value))); + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); + + if (isset($this->loadedClasses[$class])) { + return $this->loadedClasses[$class]; + } + + if (null !== $this->cache && false !== ($this->loadedClasses[$class] = $this->cache->read($class))) { + return $this->loadedClasses[$class]; + } + + if (!class_exists($class) && !interface_exists($class)) { + throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class)); + } + + $metadata = new ClassMetadata($class); + + // Include constraints from the parent class + if ($parent = $metadata->getReflectionClass()->getParentClass()) { + $metadata->mergeConstraints($this->getMetadataFor($parent->name)); + } + + // Include constraints from all implemented interfaces + foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { + if ('Symfony\Component\Validator\GroupSequenceProviderInterface' === $interface->name) { + continue; + } + $metadata->mergeConstraints($this->getMetadataFor($interface->name)); + } + + if (null !== $this->loader) { + $this->loader->loadClassMetadata($metadata); + } + + if (null !== $this->cache) { + $this->cache->write($metadata); + } + + return $this->loadedClasses[$class] = $metadata; + } + + /** + * Returns whether the factory is able to return metadata for the given + * class name or object. + * + * @param string|object $value A class name or an object + * + * @return Boolean Whether metadata can be returned for that class + */ + 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; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php new file mode 100644 index 0000000000000..ef25174d0ecc2 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Factory; + +use Symfony\Component\Validator\MetadataFactoryInterface as LegacyMetadataFactoryInterface; + +/** + * Returns {@link MetadataInterface} instances for values. + * + * @since 2.5 + * @author Bernhard Schussek + */ +interface MetadataFactoryInterface extends LegacyMetadataFactoryInterface +{ +} diff --git a/src/Symfony/Component/Validator/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/MetadataFactoryInterface.php index 40074556c00b4..b025f19ddddd4 100644 --- a/src/Symfony/Component/Validator/MetadataFactoryInterface.php +++ b/src/Symfony/Component/Validator/MetadataFactoryInterface.php @@ -15,6 +15,9 @@ * Returns {@link MetadataInterface} instances for values. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\Factory\MetadataFactoryInterface} instead. */ interface MetadataFactoryInterface { diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 8250ea6684724..1611d9b387df0 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -31,12 +31,8 @@ use Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator\LegacyValidator; use Symfony\Component\Validator\Validator\RecursiveValidator; -use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator as ValidatorV24; /** From 9b204c93544532ebac27e6b500de22a2599fab7e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 17:36:12 +0100 Subject: [PATCH 090/323] [FrameworkBundle] Implemented configuration to select the desired Validator API --- .../Compiler/AddValidatorInitializersPass.php | 6 +- .../DependencyInjection/Configuration.php | 17 ++++ .../FrameworkExtension.php | 47 +++++++-- .../Resources/config/schema/symfony-1.0.xsd | 14 +++ .../Resources/config/validator.xml | 56 ++++------- .../DependencyInjection/ConfigurationTest.php | 2 + .../Fixtures/php/validation_2_4_api.php | 9 ++ .../validation_multiple_static_methods.php | 9 ++ .../php/validation_no_static_method.php | 9 ++ .../Fixtures/xml/validation_2_4_api.xml | 12 +++ .../validation_multiple_static_methods.xml | 15 +++ .../xml/validation_no_static_method.xml | 12 +++ .../Fixtures/yml/validation_2_4_api.yml | 5 + .../validation_multiple_static_methods.yml | 5 + .../yml/validation_no_static_method.yml | 5 + .../FrameworkExtensionTest.php | 95 ++++++++++++++----- .../Validator/Tests/ValidatorBuilderTest.php | 2 +- .../Component/Validator/Validation.php | 6 ++ src/Symfony/Component/Validator/Validator.php | 12 ++- .../Validator/ValidatorInterface.php | 23 +---- .../Component/Validator/ValidatorBuilder.php | 6 +- 21 files changed, 264 insertions(+), 103 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_2_4_api.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_multiple_static_methods.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_no_static_method.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_2_4_api.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_multiple_static_methods.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_no_static_method.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_2_4_api.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_multiple_static_methods.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_no_static_method.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php index bf9f33811199a..6f58fc21bebf1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php @@ -19,15 +19,17 @@ class AddValidatorInitializersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('validator')) { + if (!$container->hasDefinition('validator.builder')) { return; } + $validatorBuilder = $container->getDefinition('validator.builder'); + $initializers = array(); foreach ($container->findTaggedServiceIds('validator.initializer') as $id => $attributes) { $initializers[] = new Reference($id); } - $container->getDefinition('validator')->replaceArgument(4, $initializers); + $validatorBuilder->addMethodCall('addObjectInitializers', array($initializers)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a317d3cc8efe1..c2639295636c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -444,8 +444,25 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('cache')->end() ->booleanNode('enable_annotations')->defaultFalse()->end() + ->arrayNode('static_method') + ->defaultValue(array('loadClassMetadata')) + ->prototype('scalar')->end() + ->treatFalseLike(array()) + ->validate() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return (array) $v; }) + ->end() + ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() ->booleanNode('strict_email')->defaultFalse()->end() + ->enumNode('api') + ->values(array('2.4', '2.5', '2.5-bc', 'auto')) + ->defaultValue('auto') + ->beforeNormalization() + ->ifTrue(function ($v) { return is_scalar($v); }) + ->then(function ($v) { return (string) $v; }) + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 5ee6982bc8d1f..94516e0463be0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -21,6 +21,7 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Validator\Validation; /** * FrameworkExtension. @@ -674,27 +675,57 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $loader->load('validator.xml'); + $validatorBuilder = $container->getDefinition('validator.builder'); + $container->setParameter('validator.translation_domain', $config['translation_domain']); - $container->setParameter('validator.mapping.loader.xml_files_loader.mapping_files', $this->getValidatorXmlMappingFiles($container)); - $container->setParameter('validator.mapping.loader.yaml_files_loader.mapping_files', $this->getValidatorYamlMappingFiles($container)); + + $xmlMappings = $this->getValidatorXmlMappingFiles($container); + $yamlMappings = $this->getValidatorYamlMappingFiles($container); + + if (count($xmlMappings) > 0) { + $validatorBuilder->addMethodCall('addXmlMappings', array($xmlMappings)); + } + + if (count($yamlMappings) > 0) { + $validatorBuilder->addMethodCall('addYamlMappings', array($yamlMappings)); + } $definition = $container->findDefinition('validator.email'); $definition->replaceArgument(0, $config['strict_email']); if (array_key_exists('enable_annotations', $config) && $config['enable_annotations']) { - $loaderChain = $container->getDefinition('validator.mapping.loader.loader_chain'); - $arguments = $loaderChain->getArguments(); - array_unshift($arguments[0], new Reference('validator.mapping.loader.annotation_loader')); - $loaderChain->setArguments($arguments); + $validatorBuilder->addMethodCall('enableAnnotations', array(new Reference('annotation_reader'))); + } + + if (array_key_exists('static_method', $config) && $config['static_method']) { + foreach ($config['static_method'] as $methodName) { + $validatorBuilder->addMethodCall('addMethodMapping', array($methodName)); + } } if (isset($config['cache'])) { - $container->getDefinition('validator.mapping.class_metadata_factory') - ->replaceArgument(1, new Reference('validator.mapping.cache.'.$config['cache'])); $container->setParameter( 'validator.mapping.cache.prefix', 'validator_'.hash('sha256', $container->getParameter('kernel.root_dir')) ); + + $validatorBuilder->addMethodCall('setCache', array(new Reference('validator.mapping.cache.'.$config['cache']))); + } + + if ('auto' !== $config['api']) { + switch ($config['api']) { + case '2.4': + $api = Validation::API_VERSION_2_4; + break; + case '2.5': + $api = Validation::API_VERSION_2_5; + break; + default: + $api = Validation::API_VERSION_2_5_BC; + break; + } + + $validatorBuilder->addMethodCall('setApiVersion', array($api)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index fcc9e254589da..639b98d6f40a8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -7,6 +7,14 @@ + + + + + + + + @@ -151,9 +159,15 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 930300f75c5d1..bf54f92603229 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -5,36 +5,34 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\Validator\Validator + Symfony\Component\Validator\ValidatorInterface + Symfony\Component\Validator\ValidatorBuilderInterface + Symfony\Component\Validator\Validation Symfony\Component\Validator\Mapping\ClassMetadataFactory Symfony\Component\Validator\Mapping\Cache\ApcCache - Symfony\Component\Validator\Mapping\Loader\LoaderChain - Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader - Symfony\Component\Validator\Mapping\Loader\AnnotationLoader - Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader - Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory - - Symfony\Component\Validator\Constraints\ExpressionValidator Symfony\Component\Validator\Constraints\EmailValidator - - - - - %validator.translation_domain% - - + - diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php new file mode 100644 index 0000000000000..e17961ac99db1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php @@ -0,0 +1,81 @@ +register('foo') + ->setPublic(false) + ; + $fooExtendedDefinition = $container + ->register('foo.extended') + ->setPublic(true) + ->setDecoratedService('foo') + ; + $barDefinition = $container + ->register('bar') + ->setPublic(true) + ; + $barExtendedDefinition = $container + ->register('bar.extended') + ->setPublic(true) + ->setDecoratedService('bar', 'bar.yoo') + ; + + $this->process($container); + + $this->assertEquals('foo.extended', $container->getAlias('foo')); + $this->assertFalse($container->getAlias('foo')->isPublic()); + + $this->assertEquals('bar.extended', $container->getAlias('bar')); + $this->assertTrue($container->getAlias('bar')->isPublic()); + + $this->assertSame($fooDefinition, $container->getDefinition('foo.extended.inner')); + $this->assertFalse($container->getDefinition('foo.extended.inner')->isPublic()); + + $this->assertSame($barDefinition, $container->getDefinition('bar.yoo')); + $this->assertFalse($container->getDefinition('bar.yoo')->isPublic()); + + $this->assertNull($fooExtendedDefinition->getDecoratedService()); + $this->assertNull($barExtendedDefinition->getDecoratedService()); + } + + public function testProcessWithAlias() + { + $container = new ContainerBuilder(); + $container + ->register('foo') + ->setPublic(true) + ; + $container->setAlias('foo.alias', new Alias('foo', false)); + $fooExtendedDefinition = $container + ->register('foo.extended') + ->setPublic(true) + ->setDecoratedService('foo.alias') + ; + + $this->process($container); + + $this->assertEquals('foo.extended', $container->getAlias('foo.alias')); + $this->assertFalse($container->getAlias('foo.alias')->isPublic()); + + $this->assertEquals('foo', $container->getAlias('foo.extended.inner')); + $this->assertFalse($container->getAlias('foo.extended.inner')->isPublic()); + + $this->assertNull($fooExtendedDefinition->getDecoratedService()); + } + + protected function process(ContainerBuilder $container) + { + $repeatedPass = new DecoratorServicePass(); + $repeatedPass->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index 4ffd7c079267a..9b4cf21ff90c7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -62,6 +62,26 @@ public function testSetGetClass() $this->assertEquals('foo', $def->getClass(), '->getClass() returns the class name'); } + public function testSetGetDecoratedService() + { + $def = new Definition('stdClass'); + $this->assertNull($def->getDecoratedService()); + $def->setDecoratedService('foo', 'foo.renamed'); + $this->assertEquals(array('foo', 'foo.renamed'), $def->getDecoratedService()); + $def->setDecoratedService(null); + $this->assertNull($def->getDecoratedService()); + + $def = new Definition('stdClass'); + $def->setDecoratedService('foo'); + $this->assertEquals(array('foo', null), $def->getDecoratedService()); + $def->setDecoratedService(null); + $this->assertNull($def->getDecoratedService()); + + $def = new Definition('stdClass'); + $this->setExpectedException('InvalidArgumentException', 'The decorated service inner name for "foo" must be different than the service name itself.'); + $def->setDecoratedService('foo', 'foo'); + } + /** * @covers Symfony\Component\DependencyInjection\Definition::setArguments * @covers Symfony\Component\DependencyInjection\Definition::getArguments diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php index 9ec3438db26bb..d3116b0a30f3e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php @@ -66,7 +66,7 @@ public function testAddService() public function testDumpAnonymousServices() { - include self::$fixturesPath.'/containers/container11.php'; + $container = include self::$fixturesPath.'/containers/container11.php'; $dumper = new XmlDumper($container); $this->assertEquals(" @@ -87,7 +87,7 @@ public function testDumpAnonymousServices() public function testDumpEntities() { - include self::$fixturesPath.'/containers/container12.php'; + $container = include self::$fixturesPath.'/containers/container12.php'; $dumper = new XmlDumper($container); $this->assertEquals(" @@ -100,4 +100,35 @@ public function testDumpEntities() ", $dumper->dump()); } + + /** + * @dataProvider provideDecoratedServicesData + */ + public function testDumpDecoratedServices($expectedXmlDump, $container) + { + $dumper = new XmlDumper($container); + $this->assertEquals($expectedXmlDump, $dumper->dump()); + } + + public function provideDecoratedServicesData() + { + $fixturesPath = realpath(__DIR__.'/../Fixtures/'); + + return array( + array(" + + + + + +", include $fixturesPath.'/containers/container15.php'), + array(" + + + + + +", include $fixturesPath.'/containers/container16.php'), + ); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container15.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container15.php new file mode 100644 index 0000000000000..bb41ea3c4f175 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container15.php @@ -0,0 +1,11 @@ +register('foo', 'FooClass\\Foo') + ->setDecoratedService('bar', 'bar.woozy') +; + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container16.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container16.php new file mode 100644 index 0000000000000..67b4d353db4d2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container16.php @@ -0,0 +1,11 @@ +register('foo', 'FooClass\\Foo') + ->setDecoratedService('bar') +; + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php index c1fceaf24f498..15ccc03d0c44f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php @@ -92,5 +92,16 @@ ->register('configured_service', 'stdClass') ->setConfigurator(array(new Reference('configurator_service'), 'configureStdClass')) ; +$container + ->register('decorated', 'stdClass') +; +$container + ->register('decorator_service', 'stdClass') + ->setDecoratedService('decorated') +; +$container + ->register('decorator_service_with_name', 'stdClass') + ->setDecoratedService('decorated', 'decorated.pif-pouf') +; return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot index 35a0692caf4a6..6bf53e56b65d5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot @@ -16,6 +16,9 @@ digraph sc { node_depends_on_request [label="depends_on_request\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_configurator_service [label="configurator_service\nConfClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_configured_service [label="configured_service\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_decorated [label="decorated\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_decorator_service [label="decorator_service\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_decorator_service_with_name [label="decorator_service_with_name\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"]; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php index a18f7437fbe7e..0e5f4dc53ef43 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php @@ -27,6 +27,9 @@ public function __construct() 'baz' => 'getBazService', 'configurator_service' => 'getConfiguratorServiceService', 'configured_service' => 'getConfiguredServiceService', + 'decorated' => 'getDecoratedService', + 'decorator_service' => 'getDecoratorServiceService', + 'decorator_service_with_name' => 'getDecoratorServiceWithNameService', 'depends_on_request' => 'getDependsOnRequestService', 'factory_service' => 'getFactoryServiceService', 'foo' => 'getFooService', @@ -96,6 +99,45 @@ protected function getConfiguredServiceService() return $instance; } + /** + * Gets the 'decorated' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDecoratedService() + { + return $this->services['decorated'] = new \stdClass(); + } + + /** + * Gets the 'decorator_service' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDecoratorServiceService() + { + return $this->services['decorator_service'] = new \stdClass(); + } + + /** + * Gets the 'decorator_service_with_name' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDecoratorServiceWithNameService() + { + return $this->services['decorator_service_with_name'] = new \stdClass(); + } + /** * Gets the 'depends_on_request' service. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index 74e518f3e1607..fcbe3dcd34079 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -37,6 +37,8 @@ public function __construct() 'bar' => 'getBarService', 'baz' => 'getBazService', 'configured_service' => 'getConfiguredServiceService', + 'decorator_service' => 'getDecoratorServiceService', + 'decorator_service_with_name' => 'getDecoratorServiceWithNameService', 'depends_on_request' => 'getDependsOnRequestService', 'factory_service' => 'getFactoryServiceService', 'foo' => 'getFooService', @@ -49,6 +51,7 @@ public function __construct() $this->aliases = array( 'alias_for_alias' => 'foo', 'alias_for_foo' => 'foo', + 'decorated' => 'decorator_service_with_name', ); } @@ -108,6 +111,32 @@ protected function getConfiguredServiceService() return $instance; } + /** + * Gets the 'decorator_service' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDecoratorServiceService() + { + return $this->services['decorator_service'] = new \stdClass(); + } + + /** + * Gets the 'decorator_service_with_name' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getDecoratorServiceWithNameService() + { + return $this->services['decorator_service_with_name'] = new \stdClass(); + } + /** * Gets the 'depends_on_request' service. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml index ffc038234fae3..7e2753eb7c73f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml @@ -50,5 +50,7 @@ + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml index f9d9d4797bebf..786bc7aae02f0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml @@ -88,6 +88,9 @@ + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml index d3c793f2e31c3..420c56d34f06f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml @@ -30,3 +30,8 @@ services: synthetic: true synchronized: true lazy: true + decorator_service: + decorates: decorated + decorator_service_with_name: + decorates: decorated + decoration-inner-name: decorated.pif-pouf diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml index 1dd163a259cca..41cc350f9e4cd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml @@ -79,5 +79,14 @@ services: configured_service: class: stdClass configurator: ['@configurator_service', configureStdClass] + decorated: + class: stdClass + decorator_service: + class: stdClass + decorates: decorated + decorator_service_with_name: + class: stdClass + decorates: decorated + decoration-inner-name: decorated.pif-pouf alias_for_foo: @foo alias_for_alias: @foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index b1f32e283e9c7..6a660a3af26c6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -229,6 +229,10 @@ public function testLoadServices() $this->assertTrue(isset($aliases['another_alias_for_foo'])); $this->assertEquals('foo', (string) $aliases['another_alias_for_foo']); $this->assertFalse($aliases['another_alias_for_foo']->isPublic()); + + $this->assertNull($services['request']->getDecoratedService()); + $this->assertEquals(array('decorated', null), $services['decorator_service']->getDecoratedService()); + $this->assertEquals(array('decorated', 'decorated.pif-pouf'), $services['decorator_service_with_name']->getDecoratedService()); } public function testParsesTags() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 37abca7483a05..ef28b57798077 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -131,6 +131,10 @@ public function testLoadServices() $this->assertTrue(isset($aliases['another_alias_for_foo'])); $this->assertEquals('foo', (string) $aliases['another_alias_for_foo']); $this->assertFalse($aliases['another_alias_for_foo']->isPublic()); + + $this->assertNull($services['request']->getDecoratedService()); + $this->assertEquals(array('decorated', null), $services['decorator_service']->getDecoratedService()); + $this->assertEquals(array('decorated', 'decorated.pif-pouf'), $services['decorator_service_with_name']->getDecoratedService()); } public function testExtensions() From 6157be0a1d7c4fc38c6cd3311ad305aa7aebadb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Garc=C3=ADa=20Sanz?= Date: Tue, 1 Apr 2014 11:04:09 +0200 Subject: [PATCH 104/323] Wrong number of parameters Wrong number of parameters Wrong number of parameters Wrong number of parameters --- .../PropertyAccess/PropertyAccessor.php | 6 ++-- .../Tests/Fixtures/TestClass.php | 36 +++++++++++++++++++ .../Tests/PropertyAccessorTest.php | 10 ++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index c52b598cb9391..a3e6c8a5dc058 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -570,7 +570,7 @@ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singula } /** - * Returns whether a method is public and has a specific number of required parameters. + * Returns whether a method is public and has the number of required parameters. * * @param \ReflectionClass $class The class of the method * @param string $methodName The method name @@ -584,7 +584,9 @@ private function isMethodAccessible(\ReflectionClass $class, $methodName, $param if ($class->hasMethod($methodName)) { $method = $class->getMethod($methodName); - if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) { + if ($method->isPublic() + && $method->getNumberOfRequiredParameters() <= $parameters + && $method->getNumberOfParameters() >= $parameters) { return true; } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php index 178d7f618abb5..9765c77da26c6 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php @@ -18,6 +18,9 @@ class TestClass private $privateProperty; private $publicAccessor; + private $publicAccessorWithDefaultValue; + private $publicAccessorWithRequiredAndDefaultValue; + private $publicAccessorWithMoreRequiredParameters; private $publicIsAccessor; private $publicHasAccessor; @@ -25,6 +28,9 @@ public function __construct($value) { $this->publicProperty = $value; $this->publicAccessor = $value; + $this->publicAccessorWithDefaultValue = $value; + $this->publicAccessorWithRequiredAndDefaultValue = $value; + $this->publicAccessorWithMoreRequiredParameters = $value; $this->publicIsAccessor = $value; $this->publicHasAccessor = $value; } @@ -34,11 +40,41 @@ public function setPublicAccessor($value) $this->publicAccessor = $value; } + public function setPublicAccessorWithDefaultValue($value = null) + { + $this->publicAccessorWithDefaultValue = $value; + } + + public function setPublicAccessorWithRequiredAndDefaultValue($value, $optional = null) + { + $this->publicAccessorWithRequiredAndDefaultValue = $value; + } + + public function setPublicAccessorWithMoreRequiredParameters($value, $needed) + { + $this->publicAccessorWithMoreRequiredParameters = $value; + } + public function getPublicAccessor() { return $this->publicAccessor; } + public function getPublicAccessorWithDefaultValue() + { + return $this->publicAccessorWithDefaultValue; + } + + public function getPublicAccessorWithRequiredAndDefaultValue() + { + return $this->publicAccessorWithRequiredAndDefaultValue; + } + + public function getPublicAccessorWithMoreRequiredParameters() + { + return $this->publicAccessorWithMoreRequiredParameters; + } + public function setPublicIsAccessor($value) { $this->publicIsAccessor = $value; diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index a6b09fa0ab7a0..6fc5f7022f525 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -43,6 +43,8 @@ public function getValidPropertyPaths() // Accessor methods array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'), array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'), array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'), array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'), @@ -250,6 +252,14 @@ public function testSetValueUpdatesMagicSet() $this->assertEquals('Updated', $author->__get('magicProperty')); } + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueThrowsExceptionIfThereAreMissingParameters() + { + $this->propertyAccessor->setValue(new TestClass('Bernhard'), 'publicAccessorWithMoreRequiredParameters', 'Updated'); + } + /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ From c413f897233a3bf1cd2e5670e5f480aefc6626b0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 13 Dec 2013 14:43:58 +0100 Subject: [PATCH 105/323] [Console] added a better way to ask questions to the user --- UPGRADE-3.0.md | 2 + src/Symfony/Component/Console/CHANGELOG.md | 4 +- .../Component/Console/Helper/DialogHelper.php | 3 + .../Console/Helper/QuestionHelper.php | 378 ++++++++++++++++++ .../Console/Question/ChoiceQuestion.php | 95 +++++ .../Console/Question/ConfirmationQuestion.php | 44 ++ .../Component/Console/Question/Question.php | 122 ++++++ 7 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Console/Helper/QuestionHelper.php create mode 100644 src/Symfony/Component/Console/Question/ChoiceQuestion.php create mode 100644 src/Symfony/Component/Console/Question/ConfirmationQuestion.php create mode 100644 src/Symfony/Component/Console/Question/Question.php diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index e9468f9ed1892..9c5c3dc475e71 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -20,6 +20,8 @@ UPGRADE FROM 2.x to 3.0 ### Console + * The `dialog` helper has been removed in favor of the `question` helper. + * The methods `isQuiet`, `isVerbose`, `isVeryVerbose` and `isDebug` were added to `Symfony\Component\Console\Output\OutputInterface`. diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 47dcebd31c5d1..9c5741b5e7279 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,10 +4,12 @@ CHANGELOG 2.5.0 ----- + * deprecated the dialog helper (use the question helper instead) * deprecated TableHelper in favor of Table * deprecated ProgressHelper in favor of ProgressBar + * added a question helper + * added a way to set the process name of a command * added a way to set a default command instead of `ListCommand` - * added a way to set the process title of a command 2.4.0 ----- diff --git a/src/Symfony/Component/Console/Helper/DialogHelper.php b/src/Symfony/Component/Console/Helper/DialogHelper.php index 7a4686fa11e7c..4ae620a0a0735 100644 --- a/src/Symfony/Component/Console/Helper/DialogHelper.php +++ b/src/Symfony/Component/Console/Helper/DialogHelper.php @@ -18,6 +18,9 @@ * The Dialog class provides helpers to interact with the user. * * @author Fabien Potencier + * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * Use the question helper instead. */ class DialogHelper extends InputAwareHelper { diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php new file mode 100644 index 0000000000000..3affb16887213 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -0,0 +1,378 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Dialog\Question; +use Symfony\Component\Console\Dialog\ChoiceQuestion; + +/** + * The Question class provides helpers to interact with the user. + * + * @author Fabien Potencier + */ +class QuestionHelper extends Helper +{ + private $inputStream; + private static $shell; + private static $stty; + + public function __construct() + { + $this->inputStream = STDIN; + } + + /** + * Asks a question to the user. + * + * @param OutputInterface $output An Output instance + * @param Question $question The question to ask + * + * @return string The user answer + * + * @throws \RuntimeException If there is no data to read in the input stream + */ + public function ask(OutputInterface $output, Question $question) + { + $that = $this; + + if (!$question->getValidator()) { + return $that->doAsk($output, $question); + } + + $interviewer = function() use ($output, $question, $that) { + return $that->doAsk($output, $question); + }; + + return $this->validateAttempts($interviewer, $output, $question); + } + + /** + * Sets the input stream to read from when interacting with the user. + * + * This is mainly useful for testing purpose. + * + * @param resource $stream The input stream + */ + public function setInputStream($stream) + { + $this->inputStream = $stream; + } + + /** + * Returns the helper's input stream + * + * @return string + */ + public function getInputStream() + { + return $this->inputStream; + } + + private function doAsk($output, $question) + { + $message = $question->getQuestion(); + if ($question instanceof ChoiceQuestion) { + $width = max(array_map('strlen', array_keys($question->getChoices()))); + + $messages = (array) $question->getQuestion(); + foreach ($question->getChoices() as $key => $value) { + $messages[] = sprintf(" [%-${width}s] %s", $key, $value); + } + + $output->writeln($messages); + + $message = $question->getPrompt(); + } + + $output->write($message); + + $autocomplete = $question->getAutocompleter(); + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($question->isHidden()) { + try { + $ret = trim($this->askHiddenResponse($output, $question)); + } catch (\RuntimeException $e) { + if (!$question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($this->inputStream, 4096); + if (false === $ret) { + throw new \RuntimeException('Aborted'); + } + $ret = trim($ret); + } + } else { + $ret = $this->autocomplete($output, $question); + } + + $ret = strlen($ret) > 0 ? $ret : $question->getDefault(); + + if ($normalizer = $question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function autocomplete(OutputInterface $output, Question $question) + { + $autocomplete = $question->getAutocompleter(); + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + + $sttyMode = shell_exec('stty -g'); + + // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) + shell_exec('stty -icanon -echo'); + + // Add highlighted text style + $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); + + // Read a keypress + while (!feof($this->inputStream)) { + $c = fread($this->inputStream, 1); + + // Backspace Character + if ("\177" === $c) { + if (0 === $numMatches && 0 !== $i) { + $i--; + // Move cursor backwards + $output->write("\033[1D"); + } + + if ($i === 0) { + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + } else { + $numMatches = 0; + } + + // Pop the last character off the end of our string + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { // Did we read an escape sequence? + $c .= fread($this->inputStream, 2); + + // A = Up Arrow. B = Down Arrow + if ('A' === $c[2] || 'B' === $c[2]) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = $matches[$ofs]; + // Echo out remaining chars for current match + $output->write(substr($ret, $i)); + $i = strlen($ret); + } + + if ("\n" === $c) { + $output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + $output->write($c); + $ret .= $c; + $i++; + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete as $value) { + // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) + if (0 === strpos($value, $ret) && $i !== strlen($value)) { + $matches[$numMatches++] = $value; + } + } + } + + // Erase characters from cursor to end of line + $output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + // Save cursor position + $output->write("\0337"); + // Write highlighted text + $output->write(''.substr($matches[$ofs], $i).''); + // Restore cursor position + $output->write("\0338"); + } + } + + // Reset stty so it behaves normally again + shell_exec(sprintf('stty %s', $sttyMode)); + + return $ret; + } + + /** + * Asks a question to the user, the response is hidden + * + * @param OutputInterface $output An Output instance + * @param string|array $question The question + * + * @return string The answer + * + * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden + */ + private function askHiddenResponse(OutputInterface $output, Question $question) + { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; + + // handle code running from a phar + if ('phar:' === substr(__FILE__, 0, 5)) { + $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; + copy($exe, $tmpExe); + $exe = $tmpExe; + } + + $value = rtrim(shell_exec($exe)); + $output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($this->inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new \RuntimeException('Aborted'); + } + + $value = trim($value); + $output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $output->writeln(''); + + return $value; + } + + throw new \RuntimeException('Unable to hide the response'); + } + + /** + * Validates an attempt. + * + * @param callable $interviewer A callable that will ask for a question and return the result + * @param OutputInterface $output An Output instance + * @param Question $question A Question instance + * + * @return string The validated response + * + * @throws \Exception In case the max number of attempts has been reached and no valid response has been given + */ + private function validateAttempts($interviewer, OutputInterface $output, Question $question) + { + $error = null; + $attempts = $question->getMaxAttemps(); + while (false === $attempts || $attempts--) { + if (null !== $error) { + $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error')); + } + + try { + return call_user_func($question->getValidator(), $interviewer()); + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * Return a valid unix shell + * + * @return string|Boolean The valid shell name, false in case no valid shell is found + */ + private function getShell() + { + if (null !== self::$shell) { + return self::$shell; + } + + self::$shell = false; + + if (file_exists('/usr/bin/env')) { + // handle other OSs with bash/zsh/ksh/csh if available to hide the answer + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + self::$shell = $sh; + break; + } + } + } + + return self::$shell; + } + + private function hasSttyAvailable() + { + if (null !== self::$stty) { + return self::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = $exitcode === 0; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'question'; + } +} diff --git a/src/Symfony/Component/Console/Question/ChoiceQuestion.php b/src/Symfony/Component/Console/Question/ChoiceQuestion.php new file mode 100644 index 0000000000000..c6589e26fc126 --- /dev/null +++ b/src/Symfony/Component/Console/Question/ChoiceQuestion.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +/** + * Represents a choice question. + * + * @author Fabien Potencier + */ +class ChoiceQuestion extends Question +{ + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + public function __construct($question, array $choices, $default = null) + { + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleter(array_keys($choices)); + } + + public function getChoices() + { + return $this->choices; + } + + public function setMultiselect($multiselect) + { + $this->multiselect = $multiselect; + } + + public function getPrompt() + { + return $this->prompt; + } + + public function setPrompt($prompt) + { + $this->prompt = $prompt; + } + + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + } + + private function getDefaultValidator() + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + + return function ($selected) use ($choices, $errorMessage, $multiselect) { + // Collapse all spaces. + $selectedChoices = str_replace(' ', '', $selected); + + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $selected)); + } + $selectedChoices = explode(',', $selectedChoices); + } else { + $selectedChoices = array($selected); + } + + $multiselectChoices = array(); + foreach ($selectedChoices as $value) { + if (empty($choices[$value])) { + throw new \InvalidArgumentException(sprintf($errorMessage, $value)); + } + array_push($multiselectChoices, $value); + } + + if ($multiselect) { + return $multiselectChoices; + } + + return $selected; + }; + } +} diff --git a/src/Symfony/Component/Console/Question/ConfirmationQuestion.php b/src/Symfony/Component/Console/Question/ConfirmationQuestion.php new file mode 100644 index 0000000000000..bbdd102be7e7c --- /dev/null +++ b/src/Symfony/Component/Console/Question/ConfirmationQuestion.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\Console\Question; + +/** + * Represents a yes/no question. + * + * @author Fabien Potencier + */ +class ConfirmationQuestion extends Question +{ + public function __construct($question, $default = false) + { + parent::__construct($question, $default); + + $this->setNormalizer($this->getDefaultNormalizer()); + } + + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + + return function ($answer) use ($default) { + if (is_bool($answer)) { + return $answer; + } + + if (false === $default) { + return $answer && 'y' == strtolower($answer[0]); + } + + return !$answer || 'y' == strtolower($answer[0]); + }; + } +} diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php new file mode 100644 index 0000000000000..ab056a5c41c9c --- /dev/null +++ b/src/Symfony/Component/Console/Question/Question.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +/** + * Represents a Question. + * + * @author Fabien Potencier + */ +class Question +{ + private $question; + private $attempts = false; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleter; + private $validator; + private $default; + private $normalizer; + + /** + * Constructor. + * + * @param string $question The question to ask to the user + * @param mixed $default The default answer to return if the user enters nothing + */ + public function __construct($question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + public function getQuestion() + { + return $this->question; + } + + public function getDefault() + { + return $this->default; + } + + public function isHidden() + { + return $this->hidden; + } + + public function setHidden($hidden) + { + if ($this->autocompleter) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = (Boolean) $hidden; + } + + public function isHiddenFallback() + { + return $this->fallback; + } + + /** + * Sets whether to fallback on non-hidden question if the response can not be hidden. + */ + public function setHiddenFallback($fallback) + { + $this->fallback = (Boolean) $fallback; + } + + public function getAutocompleter() + { + return $this->autocompleter; + } + + public function setAutocompleter($autocompleter) + { + if ($this->hidden) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleter = $autocompleter; + } + + public function setValidator($validator) + { + $this->validator = $validator; + } + + public function getValidator() + { + return $this->validator; + } + + public function setMaxAttemps($attempts) + { + $this->attempts = $attempts; + } + + public function getMaxAttemps() + { + return $this->attempts; + } + + public function setNormalizer($normalizer) + { + $this->normalizer = $normalizer; + } + + public function getNormalizer() + { + return $this->normalizer; + } +} From 336bba2fd816184c2adf9721659b3c6fe36e708d Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Tue, 1 Apr 2014 14:42:24 +0200 Subject: [PATCH 106/323] [Console] Add docblocks and unit tests to QuestionHelper --- src/Symfony/Component/Console/Application.php | 2 + .../Console/Helper/QuestionHelper.php | 96 ++++--- .../Console/Question/ChoiceQuestion.php | 49 +++- .../Console/Question/ConfirmationQuestion.php | 8 +- .../Component/Console/Question/Question.php | 139 +++++++++- .../Tests/Helper/QuestionHelperTest.php | 238 ++++++++++++++++++ 6 files changed, 483 insertions(+), 49 deletions(-) create mode 100644 src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 44b8a9393b7be..44c719d76a682 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArrayInput; @@ -968,6 +969,7 @@ protected function getDefaultHelperSet() new DialogHelper(), new ProgressHelper(), new TableHelper(), + new QuestionHelper(), )); } diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 3affb16887213..99344e9ea728f 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -11,14 +11,14 @@ namespace Symfony\Component\Console\Helper; -use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Dialog\Question; -use Symfony\Component\Console\Dialog\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Question\ChoiceQuestion; /** - * The Question class provides helpers to interact with the user. + * The QuestionHelper class provides helpers to interact with the user. * * @author Fabien Potencier */ @@ -36,22 +36,27 @@ public function __construct() /** * Asks a question to the user. * - * @param OutputInterface $output An Output instance + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance * @param Question $question The question to ask * * @return string The user answer * * @throws \RuntimeException If there is no data to read in the input stream */ - public function ask(OutputInterface $output, Question $question) + public function ask(InputInterface $input, OutputInterface $output, Question $question) { - $that = $this; + if (!$input->isInteractive()) { + return $question->getDefault(); + } if (!$question->getValidator()) { - return $that->doAsk($output, $question); + return $this->doAsk($output, $question); } - $interviewer = function() use ($output, $question, $that) { + $that = $this; + + $interviewer = function () use ($output, $question, $that) { return $that->doAsk($output, $question); }; @@ -64,23 +69,48 @@ public function ask(OutputInterface $output, Question $question) * This is mainly useful for testing purpose. * * @param resource $stream The input stream + * + * @throws \InvalidArgumentException In case the stream is not a resource */ public function setInputStream($stream) { + if (!is_resource($stream)) { + throw new \InvalidArgumentException('Input stream must be a valid resource.'); + } + $this->inputStream = $stream; } /** * Returns the helper's input stream * - * @return string + * @return resource */ public function getInputStream() { return $this->inputStream; } - private function doAsk($output, $question) + /** + * {@inheritdoc} + */ + public function getName() + { + return 'question'; + } + + /** + * Asks the question to the user. + * + * @param OutputInterface $output + * @param Question $question + * + * @return bool|mixed|null|string + * + * @throws \Exception + * @throws \RuntimeException + */ + private function doAsk(OutputInterface $output, Question $question) { $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { @@ -98,12 +128,12 @@ private function doAsk($output, $question) $output->write($message); - $autocomplete = $question->getAutocompleter(); + $autocomplete = $question->getAutocompleterValues(); if (null === $autocomplete || !$this->hasSttyAvailable()) { $ret = false; if ($question->isHidden()) { try { - $ret = trim($this->askHiddenResponse($output, $question)); + $ret = trim($this->getHiddenResponse($output)); } catch (\RuntimeException $e) { if (!$question->isHiddenFallback()) { throw $e; @@ -119,7 +149,7 @@ private function doAsk($output, $question) $ret = trim($ret); } } else { - $ret = $this->autocomplete($output, $question); + $ret = trim($this->autocomplete($output, $question)); } $ret = strlen($ret) > 0 ? $ret : $question->getDefault(); @@ -131,9 +161,17 @@ private function doAsk($output, $question) return $ret; } + /** + * Autocompletes a question. + * + * @param OutputInterface $output + * @param Question $question + * + * @return string + */ private function autocomplete(OutputInterface $output, Question $question) { - $autocomplete = $question->getAutocompleter(); + $autocomplete = $question->getAutocompleterValues(); $ret = ''; $i = 0; @@ -241,16 +279,15 @@ private function autocomplete(OutputInterface $output, Question $question) } /** - * Asks a question to the user, the response is hidden + * Gets a hidden response from user. * * @param OutputInterface $output An Output instance - * @param string|array $question The question * * @return string The answer * - * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden + * @throws \RuntimeException In case the fallback is deactivated and the response cannot be hidden */ - private function askHiddenResponse(OutputInterface $output, Question $question) + private function getHiddenResponse(OutputInterface $output) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; @@ -298,7 +335,7 @@ private function askHiddenResponse(OutputInterface $output, Question $question) return $value; } - throw new \RuntimeException('Unable to hide the response'); + throw new \RuntimeException('Unable to hide the response.'); } /** @@ -315,8 +352,8 @@ private function askHiddenResponse(OutputInterface $output, Question $question) private function validateAttempts($interviewer, OutputInterface $output, Question $question) { $error = null; - $attempts = $question->getMaxAttemps(); - while (false === $attempts || $attempts--) { + $attempts = $question->getMaxAttempts(); + while (null === $attempts || $attempts--) { if (null !== $error) { $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error')); } @@ -331,7 +368,7 @@ private function validateAttempts($interviewer, OutputInterface $output, Questio } /** - * Return a valid unix shell + * Returns a valid unix shell. * * @return string|Boolean The valid shell name, false in case no valid shell is found */ @@ -357,6 +394,11 @@ private function getShell() return self::$shell; } + /** + * Returns whether Stty is available or not. + * + * @return Boolean + */ private function hasSttyAvailable() { if (null !== self::$stty) { @@ -367,12 +409,4 @@ private function hasSttyAvailable() return self::$stty = $exitcode === 0; } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'question'; - } } diff --git a/src/Symfony/Component/Console/Question/ChoiceQuestion.php b/src/Symfony/Component/Console/Question/ChoiceQuestion.php index c6589e26fc126..57275d5728cf0 100644 --- a/src/Symfony/Component/Console/Question/ChoiceQuestion.php +++ b/src/Symfony/Component/Console/Question/ChoiceQuestion.php @@ -29,32 +29,75 @@ public function __construct($question, array $choices, $default = null) $this->choices = $choices; $this->setValidator($this->getDefaultValidator()); - $this->setAutocompleter(array_keys($choices)); + $this->setAutocompleterValues(array_keys($choices)); } + /** + * Returns available choices. + * + * @return array + */ public function getChoices() { return $this->choices; } + /** + * Sets multiselect option. + * + * When multiselect is set to true, multiple choices can be answered. + * + * @param Boolean $multiselect + * + * @return ChoiceQuestion The current instance + */ public function setMultiselect($multiselect) { $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; } + /** + * Gets the prompt for choices. + * + * @return string + */ public function getPrompt() { return $this->prompt; } + /** + * Sets the prompt for choices. + * + * @param string $prompt + * + * @return ChoiceQuestion The current instance + */ public function setPrompt($prompt) { $this->prompt = $prompt; + + return $this; } + /** + * Sets the error message for invalid values. + * + * The error message has a string placeholder (%s) for the invalid value. + * + * @param string $errorMessage + * + * @return ChoiceQuestion The current instance + */ public function setErrorMessage($errorMessage) { $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; } private function getDefaultValidator() @@ -82,14 +125,14 @@ private function getDefaultValidator() if (empty($choices[$value])) { throw new \InvalidArgumentException(sprintf($errorMessage, $value)); } - array_push($multiselectChoices, $value); + array_push($multiselectChoices, $choices[$value]); } if ($multiselect) { return $multiselectChoices; } - return $selected; + return $choices[$selected]; }; } } diff --git a/src/Symfony/Component/Console/Question/ConfirmationQuestion.php b/src/Symfony/Component/Console/Question/ConfirmationQuestion.php index bbdd102be7e7c..d14f878521a9e 100644 --- a/src/Symfony/Component/Console/Question/ConfirmationQuestion.php +++ b/src/Symfony/Component/Console/Question/ConfirmationQuestion.php @@ -18,9 +18,9 @@ */ class ConfirmationQuestion extends Question { - public function __construct($question, $default = false) + public function __construct($question, $default = true) { - parent::__construct($question, $default); + parent::__construct($question, (Boolean) $default); $this->setNormalizer($this->getDefaultNormalizer()); } @@ -35,10 +35,10 @@ private function getDefaultNormalizer() } if (false === $default) { - return $answer && 'y' == strtolower($answer[0]); + return $answer && 'y' === strtolower($answer[0]); } - return !$answer || 'y' == strtolower($answer[0]); + return !$answer || 'y' === strtolower($answer[0]); }; } } diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index ab056a5c41c9c..763a1c02ce3c5 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -10,6 +10,7 @@ */ namespace Symfony\Component\Console\Question; +use Doctrine\Common\Proxy\Exception\InvalidArgumentException; /** * Represents a Question. @@ -19,10 +20,10 @@ class Question { private $question; - private $attempts = false; + private $attempts; private $hidden = false; private $hiddenFallback = true; - private $autocompleter; + private $autocompleterValues; private $validator; private $default; private $normalizer; @@ -39,82 +40,198 @@ public function __construct($question, $default = null) $this->default = $default; } + /** + * Returns the question. + * + * @return string + */ public function getQuestion() { return $this->question; } + /** + * Returns the default answer. + * + * @return mixed + */ public function getDefault() { return $this->default; } + /** + * Returns whether the user response must be hidden. + * + * @return Boolean + */ public function isHidden() { return $this->hidden; } + /** + * Sets whether the user response must be hidden or not. + * + * @param Boolean $hidden + * + * @return Question The current instance + * + * @throws \LogicException In case the autocompleter is also used + */ public function setHidden($hidden) { - if ($this->autocompleter) { + if ($this->autocompleterValues) { throw new \LogicException('A hidden question cannot use the autocompleter.'); } $this->hidden = (Boolean) $hidden; + + return $this; } + /** + * In case the response can not be hidden, whether to fallback on non-hidden question or not. + * + * @return Boolean + */ public function isHiddenFallback() { - return $this->fallback; + return $this->hiddenFallback; } /** * Sets whether to fallback on non-hidden question if the response can not be hidden. + * + * @param Boolean $fallback + * + * @return Question The current instance */ public function setHiddenFallback($fallback) { - $this->fallback = (Boolean) $fallback; + $this->hiddenFallback = (Boolean) $fallback; + + return $this; } - public function getAutocompleter() + /** + * Gets values for the autocompleter. + * + * @return null|array|Traversable + */ + public function getAutocompleterValues() { - return $this->autocompleter; + return $this->autocompleterValues; } - public function setAutocompleter($autocompleter) + /** + * Sets values for the autocompleter. + * + * @param null|array|Traversable $values + * + * @return Question The current instance + * + * @throws \InvalidArgumentException + * @throws \LogicException + */ + public function setAutocompleterValues($values) { + if (null !== $values && !is_array($values)) { + if (!$values instanceof \Traversable || $values instanceof \Countable) { + throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); + } + } + if ($this->hidden) { throw new \LogicException('A hidden question cannot use the autocompleter.'); } - $this->autocompleter = $autocompleter; + $this->autocompleterValues = $values; + + return $this; } + /** + * Sets a validator for the question. + * + * @param null|callable $validator + * + * @return Question The current instance + */ public function setValidator($validator) { $this->validator = $validator; + + return $this; } + /** + * Gets the validator for the question + * + * @return null|callable + */ public function getValidator() { return $this->validator; } - public function setMaxAttemps($attempts) + /** + * Sets the maximum number of attempts. + * + * Null means an unlimited number of attempts. + * + * @param null|integer $attempts + * + * @return Question The current instance + * + * @throws InvalidArgumentException In case the number of attempts is invalid. + */ + public function setMaxAttempts($attempts) { + if (null !== $attempts && $attempts < 1) { + throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + $this->attempts = $attempts; + + return $this; } - public function getMaxAttemps() + /** + * Gets the maximum number of attempts. + * + * Null means an unlimited number of attempts. + * + * @return null|integer + */ + public function getMaxAttempts() { return $this->attempts; } + /** + * Sets a normalizer for the response. + * + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * + * @param string|Closure $normalizer + * + * @return Question The current instance + */ public function setNormalizer($normalizer) { $this->normalizer = $normalizer; + + return $this; } + /** + * Gets the normalizer for the response. + * + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * + * @return string|Closure + */ public function getNormalizer() { return $this->normalizer; diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php new file mode 100644 index 0000000000000..bba25375dc742 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\FormatterHelper; +use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; + +class QuestionHelperTest extends \PHPUnit_Framework_TestCase +{ + public function testAskChoice() + { + $questionHelper = new QuestionHelper(); + + $helperSet = new HelperSet(array(new FormatterHelper())); + $questionHelper->setHelperSet($helperSet); + + $heroes = array('Superman', 'Batman', 'Spiderman'); + + $questionHelper->setInputStream($this->getInputStream("\n1\n 1 \nFabien\n1\nFabien\n1\n0,2\n 0 , 2 \n\n\n")); + + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '2'); + // first answer is an empty answer, we're supposed to receive the default value + $this->assertEquals('Spiderman', $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes); + $this->assertEquals('Batman', $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('Batman', $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes); + $question->setErrorMessage('Input "%s" is not a superhero!'); + $this->assertEquals('Batman', $questionHelper->ask($this->createInputInterfaceMock(), $output = $this->createOutputInterface(), $question)); + + rewind($output->getStream()); + $stream = stream_get_contents($output->getStream()); + $this->assertContains('Input "Fabien" is not a superhero!', $stream); + + try { + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '1'); + $question->setMaxAttempts(1); + $questionHelper->ask($this->createInputInterfaceMock(), $output = $this->createOutputInterface(), $question); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertEquals('Value "Fabien" is invalid', $e->getMessage()); + } + + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, null); + $question->setMultiselect(true); + + $this->assertEquals(array('Batman'), $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals(array('Superman', 'Spiderman'), $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals(array('Superman', 'Spiderman'), $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '0,1'); + $question->setMultiselect(true); + + $this->assertEquals(array('Superman', 'Batman'), $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, ' 0 , 1 '); + $question->setMultiselect(true); + + $this->assertEquals(array('Superman', 'Batman'), $questionHelper->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + } + + public function testAsk() + { + $dialog = new QuestionHelper(); + + $dialog->setInputStream($this->getInputStream("\n8AM\n")); + + $question = new Question('What time is it?', '2PM'); + $this->assertEquals('2PM', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $question = new Question('What time is it?', '2PM'); + $this->assertEquals('8AM', $dialog->ask($this->createInputInterfaceMock(), $output = $this->createOutputInterface(), $question)); + + rewind($output->getStream()); + $this->assertEquals('What time is it?', stream_get_contents($output->getStream())); + } + + public function testAskWithAutocomplete() + { + if (!$this->hasSttyAvailable()) { + $this->markTestSkipped('`stty` is required to test autocomplete functionality'); + } + + // Acm + // AcsTest + // + // + // Test + // + // S + // F00oo + $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\n"); + + $dialog = new QuestionHelper(); + $dialog->setInputStream($inputStream); + $helperSet = new HelperSet(array(new FormatterHelper())); + $dialog->setHelperSet($helperSet); + + $question = new Question('Please select a bundle', 'FrameworkBundle'); + $question->setAutocompleterValues(array('AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle')); + + $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('AsseticBundleTest', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('FrameworkBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('SecurityBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('FooBundleTest', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('AsseticBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('FooBundle', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + } + + /** + * @group tty + */ + public function testAskHiddenResponse() + { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->markTestSkipped('This test is not supported on Windows'); + } + + $dialog = new QuestionHelper(); + $dialog->setInputStream($this->getInputStream("8AM\n")); + + $question = new Question('What time is it?'); + $question->setHidden(true); + + $this->assertEquals('8AM', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + } + + public function testAskConfirmation() + { + $dialog = new QuestionHelper(); + + $dialog->setInputStream($this->getInputStream("\n\n")); + $question = new ConfirmationQuestion('Do you like French fries?'); + $this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $question = new ConfirmationQuestion('Do you like French fries?', false); + $this->assertFalse($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $dialog->setInputStream($this->getInputStream("y\nyes\n")); + $question = new ConfirmationQuestion('Do you like French fries?', false); + $this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $question = new ConfirmationQuestion('Do you like French fries?', false); + $this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $dialog->setInputStream($this->getInputStream("n\nno\n")); + $question = new ConfirmationQuestion('Do you like French fries?', true); + $this->assertFalse($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $question = new ConfirmationQuestion('Do you like French fries?', true); + $this->assertFalse($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + } + + public function testAskAndValidate() + { + $dialog = new QuestionHelper(); + $helperSet = new HelperSet(array(new FormatterHelper())); + $dialog->setHelperSet($helperSet); + + $error = 'This is not a color!'; + $validator = function ($color) use ($error) { + if (!in_array($color, array('white', 'black'))) { + throw new \InvalidArgumentException($error); + } + + return $color; + }; + + $question = new Question('What color was the white horse of Henry IV?', 'white'); + $question->setValidator($validator); + $question->setMaxAttempts(2); + + $dialog->setInputStream($this->getInputStream("\nblack\n")); + $this->assertEquals('white', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->assertEquals('black', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + $dialog->setInputStream($this->getInputStream("green\nyellow\norange\n")); + try { + $this->assertEquals('white', $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + $this->fail(); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($error, $e->getMessage()); + } + } + + public function testNoInteraction() + { + $dialog = new QuestionHelper(); + $question = new Question('Do you have a job?', 'not yet'); + $this->assertEquals('not yet', $dialog->ask($this->createInputInterfaceMock(false), $this->createOutputInterface(), $question)); + } + + protected function getInputStream($input) + { + $stream = fopen('php://memory', 'r+', false); + fputs($stream, $input); + rewind($stream); + + return $stream; + } + + protected function createOutputInterface() + { + return new StreamOutput(fopen('php://memory', 'r+', false)); + } + + protected function createInputInterfaceMock($interactive = true) + { + $mock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); + $mock->expects($this->any()) + ->method('isInteractive') + ->will($this->returnValue($interactive)); + + return $mock; + } + + private function hasSttyAvailable() + { + exec('stty 2>&1', $output, $exitcode); + + return $exitcode === 0; + } +} From 33c91f9be8e48fb085ab28019716eddc924c5904 Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Sat, 19 Oct 2013 16:07:47 +0200 Subject: [PATCH 107/323] [DependencyInjection] Use DOM instead of SimpleXML for namespace support --- .../Loader/XmlFileLoader.php | 256 +++++++++++++----- .../Tests/Fixtures/xml/namespaces.xml | 17 ++ .../Tests/Loader/XmlFileLoaderTest.php | 12 + 3 files changed, 217 insertions(+), 68 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 5d7a5a7fd722b..7aea1325b7671 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -18,9 +18,9 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\SimpleXMLElement; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\ExpressionLanguage\Expression; /** * XmlFileLoader loads XML files service definitions. @@ -29,6 +29,8 @@ */ class XmlFileLoader extends FileLoader { + const NS = 'http://symfony.com/schema/dic/services'; + /** * Loads an XML file. * @@ -39,8 +41,7 @@ public function load($file, $type = null) { $path = $this->locator->locate($file); - $xml = $this->parseFile($path); - $xml->registerXPathNamespace('container', 'http://symfony.com/schema/dic/services'); + $xml = $this->parseFileToDOM($path); $this->container->addResource(new FileResource($path)); @@ -76,125 +77,131 @@ public function supports($resource, $type = null) /** * Parses parameters * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function parseParameters(SimpleXMLElement $xml, $file) + private function parseParameters(\DOMDocument $xml, $file) { - if (!$xml->parameters) { - return; + if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) { + $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter')); } - - $this->container->getParameterBag()->add($xml->parameters->getArgumentsAsPhp('parameter')); } /** * Parses imports * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function parseImports(SimpleXMLElement $xml, $file) + private function parseImports(\DOMDocument $xml, $file) { - if (false === $imports = $xml->xpath('//container:imports/container:import')) { + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + + if (false === $imports = $xpath->query('//container:imports/container:import')) { return; } foreach ($imports as $import) { $this->setCurrentDir(dirname($file)); - $this->import((string) $import['resource'], null, (Boolean) $import->getAttributeAsPhp('ignore-errors'), $file); + $this->import($import->getAttribute('resource'), null, (Boolean) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file); } } /** * Parses multiple definitions * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function parseDefinitions(SimpleXMLElement $xml, $file) + private function parseDefinitions(\DOMDocument $xml, $file) { - if (false === $services = $xml->xpath('//container:services/container:service')) { + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + + if (false === $services = $xpath->query('//container:services/container:service')) { return; } foreach ($services as $service) { - $this->parseDefinition((string) $service['id'], $service, $file); + $this->parseDefinition((string) $service->getAttribute('id'), $service, $file); } } /** * Parses an individual Definition * - * @param string $id - * @param SimpleXMLElement $service - * @param string $file + * @param string $id + * @param \DOMElement $service + * @param string $file */ - private function parseDefinition($id, $service, $file) + private function parseDefinition($id, \DOMElement $service, $file) { - if ((string) $service['alias']) { + if ($alias = $service->getAttribute('alias')) { $public = true; - if (isset($service['public'])) { - $public = $service->getAttributeAsPhp('public'); + if ($publicAttr = $service->getAttribute('public')) { + $public = XmlUtils::phpize($publicAttr); } - $this->container->setAlias($id, new Alias((string) $service['alias'], $public)); + $this->container->setAlias($id, new Alias($alias, $public)); return; } - if (isset($service['parent'])) { - $definition = new DefinitionDecorator((string) $service['parent']); + if ($parent = $service->getAttribute('parent')) { + $definition = new DefinitionDecorator($parent); } else { $definition = new Definition(); } foreach (array('class', 'scope', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'synchronized', 'lazy', 'abstract') as $key) { - if (isset($service[$key])) { + if ($value = $service->getAttribute($key)) { $method = 'set'.str_replace('-', '', $key); - $definition->$method((string) $service->getAttributeAsPhp($key)); + $definition->$method(XmlUtils::phpize($value)); } } - if ($service->file) { - $definition->setFile((string) $service->file); + if ($files = $this->getChildren($service, 'file')) { + $definition->setFile($files[0]->nodeValue); } - $definition->setArguments($service->getArgumentsAsPhp('argument')); - $definition->setProperties($service->getArgumentsAsPhp('property')); + $definition->setArguments($this->getArgumentsAsPhp($service, 'argument')); + $definition->setProperties($this->getArgumentsAsPhp($service, 'property')); - if (isset($service->configurator)) { - if (isset($service->configurator['function'])) { - $definition->setConfigurator((string) $service->configurator['function']); + if ($configurators = $this->getChildren($service, 'configurator')) { + $configurator = $configurators[0]; + if ($function = $configurator->getAttribute('function')) { + $definition->setConfigurator($function); } else { - if (isset($service->configurator['service'])) { - $class = new Reference((string) $service->configurator['service'], ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); + if ($childService = $configurator->getAttribute('service')) { + $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); } else { - $class = (string) $service->configurator['class']; + $class = $configurator->getAttribute('class'); } - $definition->setConfigurator(array($class, (string) $service->configurator['method'])); + $definition->setConfigurator(array($class, $configurator->getAttribute('method'))); } } - foreach ($service->call as $call) { - $definition->addMethodCall((string) $call['method'], $call->getArgumentsAsPhp('argument')); + foreach ($this->getChildren($service, 'call') as $call) { + $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument')); } - foreach ($service->tag as $tag) { + foreach ($this->getChildren($service, 'tag') as $tag) { $parameters = array(); - foreach ($tag->attributes() as $name => $value) { + foreach ($tag->attributes as $name => $node) { if ('name' === $name) { continue; } if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { - $parameters[$normalizedName] = SimpleXMLElement::phpize($value); + $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); } // keep not normalized key for BC too $parameters[$name] = SimpleXMLElement::phpize($value); } - $definition->addTag((string) $tag['name'], $parameters); +// $definition->addTag((string) $tag['name'], $parameters); + $definition->addTag($tag->getAttribute('name'), $parameters); } if (isset($service['decorates'])) { @@ -215,6 +222,22 @@ private function parseDefinition($id, $service, $file) * @throws InvalidArgumentException When loading of XML file returns error */ protected function parseFile($file) + { + $dom = $this->parseFileToDOM($file); + + return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement'); + } + + /** + * Parses a XML file to a \DOMDocument + * + * @param string $file Path to a file + * + * @return \DOMDocument + * + * @throws InvalidArgumentException When loading of XML file returns error + */ + protected function parseFileToDOM($file) { try { $dom = XmlUtils::loadFile($file, array($this, 'validateSchema')); @@ -224,41 +247,48 @@ protected function parseFile($file) $this->validateExtensions($dom, $file); - return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement'); + return $dom; } /** * Processes anonymous services * - * @param SimpleXMLElement $xml - * @param string $file + * @param \DOMDocument $xml + * @param string $file */ - private function processAnonymousServices(SimpleXMLElement $xml, $file) + private function processAnonymousServices(\DOMDocument $xml, $file) { $definitions = array(); $count = 0; + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + // anonymous services as arguments/properties - if (false !== $nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { + if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { foreach ($nodes as $node) { // give it a unique name $id = sprintf('%s_%d', hash('sha256', $file), ++$count); - $node['id'] = $id; + $node->setAttribute('id', $id); - $definitions[$id] = array($node->service, $file, false); - $node->service['id'] = $id; + if ($services = $this->getChildren($node, 'service')) { + $definitions[$id] = array($services[0], $file, false); + $services[0]->setAttribute('id', $id); + } } } // anonymous services "in the wild" - if (false !== $nodes = $xml->xpath('//container:services/container:service[not(@id)]')) { + if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) { foreach ($nodes as $node) { // give it a unique name $id = sprintf('%s_%d', hash('sha256', $file), ++$count); - $node['id'] = $id; + $node->setAttribute('id', $id); - $definitions[$id] = array($node, $file, true); - $node->service['id'] = $id; + if ($services = $this->getChildren($node, 'service')) { + $definitions[$id] = array($node, $file, true); + $services[0]->setAttribute('id', $id); + } } } @@ -266,13 +296,13 @@ private function processAnonymousServices(SimpleXMLElement $xml, $file) krsort($definitions); foreach ($definitions as $id => $def) { // anonymous services are always private - $def[0]['public'] = false; + $def[0]->setAttribute('public', false); $this->parseDefinition($id, $def[0], $def[1]); - $oNode = dom_import_simplexml($def[0]); + $oNode = $def[0]; if (true === $def[2]) { - $nNode = new \DOMElement('_services'); + $nNode = new \DOMElement('_services', null, self::NS); $oNode->parentNode->replaceChild($nNode, $oNode); $nNode->setAttribute('id', $id); } else { @@ -281,6 +311,96 @@ private function processAnonymousServices(SimpleXMLElement $xml, $file) } } + /** + * Returns arguments as valid php types. + * + * @param \DOMElement $node + * @param string $name + * @param Boolean $lowercase + * + * @return mixed + */ + private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) + { + $arguments = array(); + foreach ($this->getChildren($node, $name) as $arg) { + if ($nameAttr = $arg->getAttribute('name')) { + $arg->setAttribute('key', $nameAttr); + } + + if (!$key = $arg->getAttribute('key')) { + $key = !$arguments ? 0 : max(array_keys($arguments)) + 1; + } + + // parameter keys are case insensitive + if ('parameter' == $name && $lowercase) { + $key = strtolower($key); + } + + // this is used by DefinitionDecorator to overwrite a specific + // argument of the parent definition + if ($index = $arg->getAttribute('index')) { + $key = 'index_'.$index; + } + + switch ($arg->getAttribute('type')) { + case 'service': + $onInvalid = $arg->getAttribute('on-invalid'); + $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + if ('ignore' == $onInvalid) { + $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } elseif ('null' == $onInvalid) { + $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; + } + + if ($strict = $arg->getAttribute('strict')) { + $strict = XmlUtils::phpize($strict); + } else { + $strict = true; + } + + $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior, $strict); + break; + case 'expression': + $arguments[$key] = new Expression($arg->nodeValue); + break; + case 'collection': + $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false); + break; + case 'string': + $arguments[$key] = $arg->nodeValue; + break; + case 'constant': + $arguments[$key] = constant($arg->nodeValue); + break; + default: + $arguments[$key] = XmlUtils::phpize($arg->nodeValue); + } + } + + return $arguments; + } + + /** + * Get child elements by name + * + * @param \DOMNode $node + * @param mixed $name + * + * @return array + */ + private function getChildren(\DOMNode $node, $name) + { + $children = array(); + foreach ($node->childNodes as $child) { + if ($child instanceof \DOMElement && $child->localName === $name && $child->namespaceURI === self::NS) { + $children[] = $child; + } + } + + return $children; + } + /** * Validates a documents XML schema. * @@ -385,12 +505,12 @@ private function validateExtensions(\DOMDocument $dom, $file) /** * Loads from an extension. * - * @param SimpleXMLElement $xml + * @param \DOMDocument $xml */ - private function loadFromExtensions(SimpleXMLElement $xml) + private function loadFromExtensions(\DOMDocument $xml) { - foreach (dom_import_simplexml($xml)->childNodes as $node) { - if (!$node instanceof \DOMElement || $node->namespaceURI === 'http://symfony.com/schema/dic/services') { + foreach ($xml->documentElement->childNodes as $node) { + if (!$node instanceof \DOMElement || $node->namespaceURI === self::NS) { continue; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml new file mode 100644 index 0000000000000..5a05cedd7e79e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/namespaces.xml @@ -0,0 +1,17 @@ + + + + + + + + + foo + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 6a660a3af26c6..c6075f68ec8ce 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -432,4 +432,16 @@ public function testDocTypeIsNotAllowed() $this->assertSame('Document types are not allowed.', $e->getMessage(), '->load() throws an InvalidArgumentException if the configuration contains a document type'); } } + + public function testXmlNamespaces() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('namespaces.xml'); + $services = $container->getDefinitions(); + + $this->assertTrue(isset($services['foo']), '->load() parses elements'); + $this->assertEquals(1, count($services['foo']->getTag('foo.tag')), '->load parses elements'); + $this->assertEquals(array(array('setBar', array('foo'))), $services['foo']->getMethodCalls(), '->load() parses the tag'); + } } From a3c60c8039d90a4a9da3325c90b2b16a20e3f53f Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Wed, 2 Apr 2014 17:31:05 +0200 Subject: [PATCH 108/323] [DependencyInjection] Deprecate SimpleXMLElement --- .../DependencyInjection/CHANGELOG.md | 1 + .../Loader/XmlFileLoader.php | 27 ++++--------------- .../DependencyInjection/SimpleXMLElement.php | 2 ++ 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 25e7fc66aaafa..5a88e34e47d50 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added DecoratorServicePass and a way to override a service definition (Definition::setDecoratedService()) +* deprecated SimpleXMLElement class. 2.4.0 ----- diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 7aea1325b7671..58c8301065315 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -197,37 +197,20 @@ private function parseDefinition($id, \DOMElement $service, $file) $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); } // keep not normalized key for BC too - $parameters[$name] = SimpleXMLElement::phpize($value); + $parameters[$name] = XmlUtils::phpize($node->nodeValue); } -// $definition->addTag((string) $tag['name'], $parameters); $definition->addTag($tag->getAttribute('name'), $parameters); } - if (isset($service['decorates'])) { - $renameId = isset($service['decoration-inner-name']) ? (string) $service['decoration-inner-name'] : null; - $definition->setDecoratedService((string) $service['decorates'], $renameId); + if ($value = $service->getAttribute('decorates')) { + $renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null; + $definition->setDecoratedService($value, $renameId); } $this->container->setDefinition($id, $definition); } - /** - * Parses a XML file. - * - * @param string $file Path to a file - * - * @return SimpleXMLElement - * - * @throws InvalidArgumentException When loading of XML file returns error - */ - protected function parseFile($file) - { - $dom = $this->parseFileToDOM($file); - - return simplexml_import_dom($dom, 'Symfony\\Component\\DependencyInjection\\SimpleXMLElement'); - } - /** * Parses a XML file to a \DOMDocument * @@ -237,7 +220,7 @@ protected function parseFile($file) * * @throws InvalidArgumentException When loading of XML file returns error */ - protected function parseFileToDOM($file) + private function parseFileToDOM($file) { try { $dom = XmlUtils::loadFile($file, array($this, 'validateSchema')); diff --git a/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php b/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php index db855f66d1e89..27c33e9a02752 100644 --- a/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php +++ b/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php @@ -18,6 +18,8 @@ * SimpleXMLElement class. * * @author Fabien Potencier + * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. */ class SimpleXMLElement extends \SimpleXMLElement { From 099e480c1c8110a01635af7246dbc562c0dc3fcf Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Wed, 2 Apr 2014 18:29:34 +0200 Subject: [PATCH 109/323] Fix travis build --- .../FrameworkExtensionTest.php | 4 ++-- .../Console/Helper/QuestionHelper.php | 4 +++- .../Tests/Loader/XmlFileLoaderTest.php | 24 +++++++++---------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 55cd188c7cfb6..eb54c612f252b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -302,7 +302,7 @@ public function testValidationAnnotations() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); $this->assertCount(6, $calls); - $this->assertSame('enableAnnotations', $calls[4][0]); + $this->assertSame('enableAnnotationMapping', $calls[4][0]); $this->assertEquals(array(new Reference('annotation_reader')), $calls[4][1]); $this->assertSame('addMethodMapping', $calls[5][0]); $this->assertSame(array('loadClassMetadata'), $calls[5][1]); @@ -322,7 +322,7 @@ public function testValidationPaths() $this->assertCount(7, $calls); $this->assertSame('addXmlMappings', $calls[3][0]); $this->assertSame('addYamlMappings', $calls[4][0]); - $this->assertSame('enableAnnotations', $calls[5][0]); + $this->assertSame('enableAnnotationMapping', $calls[5][0]); $this->assertSame('addMethodMapping', $calls[6][0]); $this->assertSame(array('loadClassMetadata'), $calls[6][1]); diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 99344e9ea728f..57dc5db45e81f 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -102,6 +102,8 @@ public function getName() /** * Asks the question to the user. * + * This method is public for PHP 5.3 compatibility, it should be private. + * * @param OutputInterface $output * @param Question $question * @@ -110,7 +112,7 @@ public function getName() * @throws \Exception * @throws \RuntimeException */ - private function doAsk(OutputInterface $output, Question $question) + public function doAsk(OutputInterface $output, Question $question) { $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index c6075f68ec8ce..1721bc3d9c4d9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -53,37 +53,37 @@ public function testParseFile() { $loader = new XmlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/ini')); $r = new \ReflectionObject($loader); - $m = $r->getMethod('parseFile'); + $m = $r->getMethod('parseFileToDOM'); $m->setAccessible(true); try { $m->invoke($loader, self::$fixturesPath.'/ini/parameters.ini'); - $this->fail('->parseFile() throws an InvalidArgumentException if the loaded file is not a valid XML file'); + $this->fail('->parseFileToDOM() throws an InvalidArgumentException if the loaded file is not a valid XML file'); } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\Exception\\InvalidArgumentException', $e, '->parseFile() throws an InvalidArgumentException if the loaded file is not a valid XML file'); - $this->assertRegExp(sprintf('#^Unable to parse file ".+%s".$#', 'parameters.ini'), $e->getMessage(), '->parseFile() throws an InvalidArgumentException if the loaded file is not a valid XML file'); + $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\Exception\\InvalidArgumentException', $e, '->parseFileToDOM() throws an InvalidArgumentException if the loaded file is not a valid XML file'); + $this->assertRegExp(sprintf('#^Unable to parse file ".+%s".$#', 'parameters.ini'), $e->getMessage(), '->parseFileToDOM() throws an InvalidArgumentException if the loaded file is not a valid XML file'); $e = $e->getPrevious(); - $this->assertInstanceOf('InvalidArgumentException', $e, '->parseFile() throws an InvalidArgumentException if the loaded file is not a valid XML file'); - $this->assertStringStartsWith('[ERROR 4] Start tag expected, \'<\' not found (in', $e->getMessage(), '->parseFile() throws an InvalidArgumentException if the loaded file is not a valid XML file'); + $this->assertInstanceOf('InvalidArgumentException', $e, '->parseFileToDOM() throws an InvalidArgumentException if the loaded file is not a valid XML file'); + $this->assertStringStartsWith('[ERROR 4] Start tag expected, \'<\' not found (in', $e->getMessage(), '->parseFileToDOM() throws an InvalidArgumentException if the loaded file is not a valid XML file'); } $loader = new XmlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/xml')); try { $m->invoke($loader, self::$fixturesPath.'/xml/nonvalid.xml'); - $this->fail('->parseFile() throws an InvalidArgumentException if the loaded file does not validate the XSD'); + $this->fail('->parseFileToDOM() throws an InvalidArgumentException if the loaded file does not validate the XSD'); } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\Exception\\InvalidArgumentException', $e, '->parseFile() throws an InvalidArgumentException if the loaded file does not validate the XSD'); - $this->assertRegExp(sprintf('#^Unable to parse file ".+%s".$#', 'nonvalid.xml'), $e->getMessage(), '->parseFile() throws an InvalidArgumentException if the loaded file is not a valid XML file'); + $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\Exception\\InvalidArgumentException', $e, '->parseFileToDOM() throws an InvalidArgumentException if the loaded file does not validate the XSD'); + $this->assertRegExp(sprintf('#^Unable to parse file ".+%s".$#', 'nonvalid.xml'), $e->getMessage(), '->parseFileToDOM() throws an InvalidArgumentException if the loaded file is not a valid XML file'); $e = $e->getPrevious(); - $this->assertInstanceOf('InvalidArgumentException', $e, '->parseFile() throws an InvalidArgumentException if the loaded file does not validate the XSD'); - $this->assertStringStartsWith('[ERROR 1845] Element \'nonvalid\': No matching global declaration available for the validation root. (in', $e->getMessage(), '->parseFile() throws an InvalidArgumentException if the loaded file does not validate the XSD'); + $this->assertInstanceOf('InvalidArgumentException', $e, '->parseFileToDOM() throws an InvalidArgumentException if the loaded file does not validate the XSD'); + $this->assertStringStartsWith('[ERROR 1845] Element \'nonvalid\': No matching global declaration available for the validation root. (in', $e->getMessage(), '->parseFileToDOM() throws an InvalidArgumentException if the loaded file does not validate the XSD'); } $xml = $m->invoke($loader, self::$fixturesPath.'/xml/services1.xml'); - $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\SimpleXMLElement', $xml, '->parseFile() returns an SimpleXMLElement object'); + $this->assertInstanceOf('DOMDocument', $xml, '->parseFileToDOM() returns an SimpleXMLElement object'); } public function testLoadParameters() From 8163427b71037e4a14a4b58d564056a1be16eb3d Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 3 Apr 2014 12:22:56 +0200 Subject: [PATCH 110/323] [DependencyInjection] Fix #10626, use better check on attribute existence and values on XML load --- .../Loader/XmlFileLoader.php | 12 +++++++----- .../Tests/Fixtures/xml/services14.xml | 19 +++++++++++++++++++ .../Tests/Loader/XmlFileLoaderTest.php | 9 +++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services14.xml diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 58c8301065315..f688cfab73556 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -307,12 +307,14 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) { $arguments = array(); foreach ($this->getChildren($node, $name) as $arg) { - if ($nameAttr = $arg->getAttribute('name')) { - $arg->setAttribute('key', $nameAttr); + if ($arg->hasAttribute('name')) { + $arg->setAttribute('key', $arg->getAttribute('name')); } - if (!$key = $arg->getAttribute('key')) { + if (!$arg->hasAttribute('key')) { $key = !$arguments ? 0 : max(array_keys($arguments)) + 1; + } else { + $key = $arg->getAttribute('key'); } // parameter keys are case insensitive @@ -322,8 +324,8 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) // this is used by DefinitionDecorator to overwrite a specific // argument of the parent definition - if ($index = $arg->getAttribute('index')) { - $key = 'index_'.$index; + if ($arg->hasAttribute('index')) { + $key = 'index_'.$arg->getAttribute('index'); } switch ($arg->getAttribute('type')) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services14.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services14.xml new file mode 100644 index 0000000000000..73446214e4920 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services14.xml @@ -0,0 +1,19 @@ + + + + + app + + + + + + app + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 1721bc3d9c4d9..c9ad753c9bede 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -444,4 +444,13 @@ public function testXmlNamespaces() $this->assertEquals(1, count($services['foo']->getTag('foo.tag')), '->load parses elements'); $this->assertEquals(array(array('setBar', array('foo'))), $services['foo']->getMethodCalls(), '->load() parses the tag'); } + + public function testLoadIndexedArguments() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services14.xml'); + + $this->assertEquals(array('index_0' => 'app'), $container->findDefinition('logger')->getArguments()); + } } From 009c4b845194b8363711f446badb32c4c4b73467 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Thu, 3 Apr 2014 17:10:06 +0100 Subject: [PATCH 111/323] [FrameworkBundle] Only initialize a fully configured service if APC is available. At the moment we only provide ApcCache for mapping caching (out of the box). DoctrineCache is available but not configured. --- .../FrameworkExtensionTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index eb54c612f252b..7bed3b042ce0b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -277,6 +277,23 @@ public function testValidation() $this->assertSame(array('loadClassMetadata'), $calls[4][1]); $this->assertSame('setMetadataCache', $calls[5][0]); $this->assertEquals(array(new Reference('validator.mapping.cache.apc')), $calls[5][1]); + } + + public function testFullyConfiguredValidationService() + { + if (!extension_loaded('apc')) { + $this->markTestSkipped('The apc extension is not available.'); + } + + $container = $this->createContainerFromFile('full'); + + $this->assertInstanceOf('Symfony\Component\Validator\ValidatorInterface', $container->get('validator')); + } + + public function testValidationService() + { + $container = $this->createContainerFromFile('validation_annotations'); + $this->assertInstanceOf('Symfony\Component\Validator\ValidatorInterface', $container->get('validator')); } From 46bb8b99dee043a79d492560e408e34937a739dc Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Wed, 9 Apr 2014 09:13:16 +0100 Subject: [PATCH 112/323] Fixed typo --- .../Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php index 5652af57dd23c..eddf7adcc0694 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Extension/AssetsExtensionTest.php @@ -31,7 +31,7 @@ public function testGetAssetUrl($path, $packageName, $absolute, $relativeUrl, $e $this->assertEquals($expectedUrl, $extension->getAssetUrl($path, $packageName, $absolute)); } - public function testGetAssetWithtoutHost() + public function testGetAssetWithoutHost() { $path = '/path/to/asset'; $packageName = null; From db44f0fdd7011c4880b3fe6d9e6ea33da9b277a3 Mon Sep 17 00:00:00 2001 From: Charles Sarrazin Date: Wed, 9 Apr 2014 16:15:03 +0200 Subject: [PATCH 113/323] [Serializer] Refactored XmlEncoder to remove dependency to SimpleXml --- .../Serializer/Encoder/XmlEncoder.php | 148 ++++++++++++------ 1 file changed, 101 insertions(+), 47 deletions(-) diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index dd6c5f86adfe1..0ef07aeee383c 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -94,26 +94,27 @@ public function decode($data, $format, array $context = array()) } } - $xml = simplexml_import_dom($dom); + $rootNode = $dom->firstChild; - if ($error = libxml_get_last_error()) { - throw new UnexpectedValueException($error->message); + // todo: throw an exception if the root node name is not correctly configured (bc) + + if ($rootNode->hasChildNodes()) { + return $this->parseXml($rootNode); } - if (!$xml->count()) { - if (!$xml->attributes()) { - return (string) $xml; - } - $data = array(); - foreach ($xml->attributes() as $attrkey => $attr) { - $data['@'.$attrkey] = (string) $attr; - } - $data['#'] = (string) $xml; + if (!$rootNode->hasAttributes()) { + return $rootNode->nodeValue; + } - return $data; + $data = array(); + + foreach ($rootNode->attributes as $attrKey => $attr) { + $data['@'.$attrKey] = $attr->nodeValue; } - return $this->parseXml($xml); + $data['#'] = $rootNode->nodeValue; + + return $data; } /** @@ -230,54 +231,107 @@ final protected function isElementNameValid($name) } /** - * Parse the input SimpleXmlElement into an array. + * Parse the input DOMNode into an array. * - * @param \SimpleXmlElement $node xml to parse + * @param \DOMNode $node xml to parse * * @return array */ - private function parseXml(\SimpleXmlElement $node) + private function parseXml(\DOMNode $node) { - $data = array(); - if ($node->attributes()) { - foreach ($node->attributes() as $attrkey => $attr) { - $data['@'.$attrkey] = (string) $attr; - } + $data = $this->parseXmlAttributes($node); + + $value = $this->parseXmlValue($node); + + if (!count($data)) { + return $value; } - foreach ($node->children() as $key => $subnode) { - if ($subnode->count()) { - $value = $this->parseXml($subnode); - } elseif ($subnode->attributes()) { - $value = array(); - foreach ($subnode->attributes() as $attrkey => $attr) { - $value['@'.$attrkey] = (string) $attr; - } - $value['#'] = (string) $subnode; + + if (!is_array($value)) { + $data['#'] = $value; + + return $data; + } + + if (1 === count($value) && key($value)) { + $data[key($value)] = current($value); + + return $data; + } + + foreach ($value as $key => $val) { + $data[$key] = $val; + } + + return $data; + } + + /** + * Parse the input DOMNode attributes into an array + * + * @param \DOMNode $node xml to parse + * + * @return array + */ + private function parseXmlAttributes(\DOMNode $node) + { + if (!$node->hasAttributes()) { + return array(); + } + + $data = array(); + + foreach ($node->attributes as $attrkey => $attr) { + if (ctype_digit($attr->nodeValue)) { + $data['@'.$attrkey] = (int) $attr->nodeValue; } else { - $value = (string) $subnode; + $data['@'.$attrkey] = $attr->nodeValue; } + } - if ($key === 'item') { - if (isset($value['@key'])) { - if (isset($value['#'])) { - $data[$value['@key']] = $value['#']; - } else { - $data[$value['@key']] = $value; - } + return $data; + } + + /** + * Parse the input DOMNode value (content and children) into an array or a string + * + * @param \DOMNode $node xml to parse + * + * @return array|string + */ + private function parseXmlValue(\DOMNode $node) + { + if (!$node->hasChildNodes()) { + return $node->nodeValue; + } + + if (1 === $node->childNodes->length && XML_TEXT_NODE === $node->firstChild->nodeType) { + return $node->firstChild->nodeValue; + } + + $value = array(); + + foreach ($node->childNodes as $subnode) { + $val = $this->parseXml($subnode); + + if ('item' === $subnode->nodeName && isset($val['@key'])) { + if (isset($val['#'])) { + $value[$val['@key']] = $val['#']; } else { - $data['item'][] = $value; + $value[$val['@key']] = $val; } - } elseif (array_key_exists($key, $data) || $key == "entry") { - if ((false === is_array($data[$key])) || (false === isset($data[$key][0]))) { - $data[$key] = array($data[$key]); - } - $data[$key][] = $value; } else { - $data[$key] = $value; + $value[$subnode->nodeName][] = $val; } } - return $data; + foreach ($value as $key => $val) { + if (is_array($val) && 1 === count($val)) { + $value[$key] = current($val); + } + } + + return $value; } /** From 9ba2861408be53e8fd5e87629fee6f6b6ca24e3b Mon Sep 17 00:00:00 2001 From: WouterJ Date: Thu, 10 Apr 2014 14:44:40 +0200 Subject: [PATCH 114/323] Fixed Yaml loader/dumper to use underscores --- src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php | 2 +- .../Component/DependencyInjection/Loader/YamlFileLoader.php | 2 +- .../DependencyInjection/Tests/Fixtures/yaml/services6.yml | 2 +- .../DependencyInjection/Tests/Fixtures/yaml/services9.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 9ea4d16b3ca75..24fe3cf742ef3 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -143,7 +143,7 @@ private function addService($id, $definition) list ($decorated, $renamedId) = $decorated; $code .= sprintf(" decorates: %s\n", $decorated); if (null !== $renamedId) { - $code .= sprintf(" decoration-inner-name: %s\n", $renamedId); + $code .= sprintf(" decoration_inner_name: %s\n", $renamedId); } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 394825f57ee30..72f07dd2ae910 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -235,7 +235,7 @@ private function parseDefinition($id, $service, $file) } if (isset($service['decorates'])) { - $renameId = isset($service['decoration-inner-name']) ? $service['decoration-inner-name'] : null; + $renameId = isset($service['decoration_inner_name']) ? $service['decoration_inner_name'] : null; $definition->setDecoratedService($service['decorates'], $renameId); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml index 420c56d34f06f..885b6c327a6fd 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml @@ -34,4 +34,4 @@ services: decorates: decorated decorator_service_with_name: decorates: decorated - decoration-inner-name: decorated.pif-pouf + decoration_inner_name: decorated.pif-pouf diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml index 41cc350f9e4cd..0422be8dd7886 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml @@ -87,6 +87,6 @@ services: decorator_service_with_name: class: stdClass decorates: decorated - decoration-inner-name: decorated.pif-pouf + decoration_inner_name: decorated.pif-pouf alias_for_foo: @foo alias_for_alias: @foo From f1a7361aaecd9769747ca50ce9ce3ee3ac3e042a Mon Sep 17 00:00:00 2001 From: Charles Sarrazin Date: Fri, 11 Apr 2014 10:02:41 +0200 Subject: [PATCH 115/323] Fixed wrong typehint in documentation for XmlEncoder --- src/Symfony/Component/Serializer/Encoder/XmlEncoder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 0ef07aeee383c..67a0fdcd94d2f 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -231,11 +231,11 @@ final protected function isElementNameValid($name) } /** - * Parse the input DOMNode into an array. + * Parse the input DOMNode into an array or a string. * * @param \DOMNode $node xml to parse * - * @return array + * @return array|string */ private function parseXml(\DOMNode $node) { From dd957bca47175be02a7751da5bf09cbb0304a475 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 11 Apr 2014 21:14:26 +0200 Subject: [PATCH 116/323] fixed a test --- .../HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php index 02a85b9cd8ba7..8504fb88144bc 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php @@ -42,7 +42,7 @@ public function testCollect() $this->assertSame(array('name' => 'foo'), $c->getRouteParams()); $this->assertSame(array(), $c->getSessionAttributes()); $this->assertSame('en', $c->getLocale()); - $this->assertSame('Resource(stream)', $attributes->get('resource')); + $this->assertSame('Resource(stream#86)', $attributes->get('resource')); $this->assertSame('Object(stdClass)', $attributes->get('object')); $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag', $c->getResponseHeaders()); From 203a532257c46ad08704cb3f4b9721864a9bf6ab Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 11 Apr 2014 21:30:22 +0200 Subject: [PATCH 117/323] fixed a test --- .../HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php index 8504fb88144bc..f542774524011 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php @@ -42,7 +42,7 @@ public function testCollect() $this->assertSame(array('name' => 'foo'), $c->getRouteParams()); $this->assertSame(array(), $c->getSessionAttributes()); $this->assertSame('en', $c->getLocale()); - $this->assertSame('Resource(stream#86)', $attributes->get('resource')); + $this->assertRegExp('/Resource\(stream#\d+\)/', $attributes->get('resource')); $this->assertSame('Object(stdClass)', $attributes->get('object')); $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag', $c->getResponseHeaders()); From 63086a01f4cc1aa1300af8ffdc19f8364873ffdb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 11 Apr 2014 22:04:01 +0200 Subject: [PATCH 118/323] updated CHANGELOG for 2.5.0-BETA1 --- CHANGELOG-2.5.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CHANGELOG-2.5.md diff --git a/CHANGELOG-2.5.md b/CHANGELOG-2.5.md new file mode 100644 index 0000000000000..3dc59e0b13ad3 --- /dev/null +++ b/CHANGELOG-2.5.md @@ -0,0 +1,13 @@ +CHANGELOG for 2.5.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 2.5 minor versions. + +To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash +To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.5.0...v2.5.1 + +* 2.5.0-BETA1 (2014-04-11) + + * first beta release + From 2aea588ceba4cfc3e3ad39bfe9604bd025826993 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 11 Apr 2014 22:04:22 +0200 Subject: [PATCH 119/323] updated VERSION for 2.5.0-BETA1 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 603f5b6520b3d..21c72ae08dfea 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-DEV'; + const VERSION = '2.5.0-BETA1'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = 'BETA1'; /** * Constructor. From c875acf3a264f852e4bb2ddc5987bb5426d41cda Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 12 Apr 2014 16:58:29 +0200 Subject: [PATCH 120/323] bumped Symfony version to 2.5.0 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 21c72ae08dfea..603f5b6520b3d 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-BETA1'; + const VERSION = '2.5.0-DEV'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'BETA1'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From 24dde55feb895a67e412be5d84d0f1ca14142644 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 16 Apr 2014 10:09:01 +0200 Subject: [PATCH 121/323] made types consistent with those defined in Hack --- .../FrameworkBundle/Command/CacheClearCommand.php | 2 +- .../Bundle/TwigBundle/Extension/AssetsExtension.php | 4 ++-- src/Symfony/Component/ClassLoader/Psr4ClassLoader.php | 2 +- src/Symfony/Component/Console/Helper/ProgressBar.php | 10 +++++----- src/Symfony/Component/Console/Helper/Table.php | 6 +++--- src/Symfony/Component/Console/Helper/TableStyle.php | 4 ++-- .../Component/Console/Question/ChoiceQuestion.php | 2 +- .../Console/Question/ConfirmationQuestion.php | 2 +- src/Symfony/Component/Console/Question/Question.php | 10 +++++----- .../DependencyInjection/Loader/XmlFileLoader.php | 2 +- src/Symfony/Component/Form/Form.php | 2 +- src/Symfony/Component/Form/FormErrorIterator.php | 6 +++--- src/Symfony/Component/Form/FormInterface.php | 4 ++-- src/Symfony/Component/HttpFoundation/JsonResponse.php | 4 ++-- .../HttpKernel/DataCollector/Util/ValueExporter.php | 4 ++-- .../Exception/UnprocessableEntityHttpException.php | 2 +- src/Symfony/Component/Process/Process.php | 4 ++-- src/Symfony/Component/Process/ProcessPipes.php | 2 +- src/Symfony/Component/Templating/Asset/Package.php | 2 +- .../Component/Templating/Helper/CoreAssetsHelper.php | 2 +- .../Component/Validator/Constraints/GroupSequence.php | 8 ++++---- .../Component/Validator/Mapping/MemberMetadata.php | 4 ++-- .../Validator/RecursiveContextualValidator.php | 10 +++++----- .../Component/Validator/ValidatorBuilderInterface.php | 2 +- .../Violation/ConstraintViolationBuilderInterface.php | 2 +- 25 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index efe27319bb5e2..1ff6fa308cc8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -110,7 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output) /** * @param string $warmupDir * @param string $realCacheDir - * @param Boolean $enableOptionalWarmers + * @param bool $enableOptionalWarmers */ protected function warmup($warmupDir, $realCacheDir, $enableOptionalWarmers = true) { diff --git a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php index 0bcfd1199433d..05f8b0665e2a0 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php @@ -50,8 +50,8 @@ public function getFunctions() * * @param string $path A public path * @param string $packageName The name of the asset package to use - * @param Boolean $absolute Whether to return an absolute URL or a relative one - * @param string|Boolean|null $version A specific version + * @param bool $absolute Whether to return an absolute URL or a relative one + * @param string|bool|null $version A specific version * * @return string A public path which takes into account the base path and URL path */ diff --git a/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php b/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php index a705a7f7bf391..60aa80279b582 100644 --- a/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php +++ b/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php @@ -77,7 +77,7 @@ public function loadClass($class) /** * Registers this instance as an autoloader. * - * @param Boolean $prepend + * @param bool $prepend */ public function register($prepend = false) { diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 25b9c2052b7ef..4790270bd474e 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -51,7 +51,7 @@ class ProgressBar * Constructor. * * @param OutputInterface $output An OutputInterface instance - * @param integer $max Maximum steps (0 if unknown) + * @param int $max Maximum steps (0 if unknown) */ public function __construct(OutputInterface $output, $max = 0) { @@ -200,7 +200,7 @@ public function getProgressPercent() /** * Sets the progress bar width. * - * @param integer $size The progress bar size + * @param int $size The progress bar size */ public function setBarWidth($size) { @@ -299,7 +299,7 @@ public function setFormat($format) /** * Sets the redraw frequency. * - * @param integer $freq The frequency in steps + * @param int $freq The frequency in steps */ public function setRedrawFrequency($freq) { @@ -328,7 +328,7 @@ public function start() /** * Advances the progress output X steps. * - * @param integer $step Number of steps to advance + * @param int $step Number of steps to advance * * @throws \LogicException */ @@ -340,7 +340,7 @@ public function advance($step = 1) /** * Sets the current progress. * - * @param integer $step The current progress + * @param int $step The current progress * * @throws \LogicException */ diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 2548d8e57a90a..f3cd7ce8e078d 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -295,7 +295,7 @@ private function renderRow(array $row, $cellFormat) * Renders table cell with padding. * * @param array $row - * @param integer $column + * @param int $column * @param string $cellFormat */ private function renderCell(array $row, $column, $cellFormat) @@ -337,7 +337,7 @@ private function getNumberOfColumns() /** * Gets column width. * - * @param integer $column + * @param int $column * * @return integer */ @@ -363,7 +363,7 @@ private function getColumnWidth($column) * Gets cell width. * * @param array $row - * @param integer $column + * @param int $column * * @return integer */ diff --git a/src/Symfony/Component/Console/Helper/TableStyle.php b/src/Symfony/Component/Console/Helper/TableStyle.php index 179749a4cd14b..2379d9b1afa8c 100644 --- a/src/Symfony/Component/Console/Helper/TableStyle.php +++ b/src/Symfony/Component/Console/Helper/TableStyle.php @@ -228,7 +228,7 @@ public function getBorderFormat() /** * Sets cell padding type. * - * @param integer $padType STR_PAD_* + * @param int $padType STR_PAD_* * * @return TableStyle */ @@ -242,7 +242,7 @@ public function setPadType($padType) /** * Gets cell padding type. * - * @param integer + * @param int */ public function getPadType() { diff --git a/src/Symfony/Component/Console/Question/ChoiceQuestion.php b/src/Symfony/Component/Console/Question/ChoiceQuestion.php index 57275d5728cf0..59808e1256430 100644 --- a/src/Symfony/Component/Console/Question/ChoiceQuestion.php +++ b/src/Symfony/Component/Console/Question/ChoiceQuestion.php @@ -47,7 +47,7 @@ public function getChoices() * * When multiselect is set to true, multiple choices can be answered. * - * @param Boolean $multiselect + * @param bool $multiselect * * @return ChoiceQuestion The current instance */ diff --git a/src/Symfony/Component/Console/Question/ConfirmationQuestion.php b/src/Symfony/Component/Console/Question/ConfirmationQuestion.php index d14f878521a9e..0438640fa4a92 100644 --- a/src/Symfony/Component/Console/Question/ConfirmationQuestion.php +++ b/src/Symfony/Component/Console/Question/ConfirmationQuestion.php @@ -20,7 +20,7 @@ class ConfirmationQuestion extends Question { public function __construct($question, $default = true) { - parent::__construct($question, (Boolean) $default); + parent::__construct($question, (bool) $default); $this->setNormalizer($this->getDefaultNormalizer()); } diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index 763a1c02ce3c5..2fd8283f3ae69 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -73,7 +73,7 @@ public function isHidden() /** * Sets whether the user response must be hidden or not. * - * @param Boolean $hidden + * @param bool $hidden * * @return Question The current instance * @@ -85,7 +85,7 @@ public function setHidden($hidden) throw new \LogicException('A hidden question cannot use the autocompleter.'); } - $this->hidden = (Boolean) $hidden; + $this->hidden = (bool) $hidden; return $this; } @@ -103,13 +103,13 @@ public function isHiddenFallback() /** * Sets whether to fallback on non-hidden question if the response can not be hidden. * - * @param Boolean $fallback + * @param bool $fallback * * @return Question The current instance */ public function setHiddenFallback($fallback) { - $this->hiddenFallback = (Boolean) $fallback; + $this->hiddenFallback = (bool) $fallback; return $this; } @@ -180,7 +180,7 @@ public function getValidator() * * Null means an unlimited number of attempts. * - * @param null|integer $attempts + * @param null|int $attempts * * @return Question The current instance * diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 14378e50bf6f4..bdcc6e59190a2 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -299,7 +299,7 @@ private function processAnonymousServices(\DOMDocument $xml, $file) * * @param \DOMElement $node * @param string $name - * @param Boolean $lowercase + * @param bool $lowercase * * @return mixed */ diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index ec0ad9a20d70d..4fb1b1ee65f28 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -1131,7 +1131,7 @@ private function viewToNorm($value) * Utility function for indenting multi-line strings. * * @param string $string The string - * @param integer $level The number of spaces to use for indentation + * @param int $level The number of spaces to use for indentation * * @return string The indented string */ diff --git a/src/Symfony/Component/Form/FormErrorIterator.php b/src/Symfony/Component/Form/FormErrorIterator.php index bc2bdb1e506d4..83d7aefc367c0 100644 --- a/src/Symfony/Component/Form/FormErrorIterator.php +++ b/src/Symfony/Component/Form/FormErrorIterator.php @@ -159,7 +159,7 @@ public function rewind() /** * Returns whether a position exists in the iterator. * - * @param integer $position The position + * @param int $position The position * * @return Boolean Whether that position exists */ @@ -171,7 +171,7 @@ public function offsetExists($position) /** * Returns the element at a position in the iterator. * - * @param integer $position The position + * @param int $position The position * * @return FormError|FormErrorIterator The element at the given position * @@ -250,7 +250,7 @@ public function count() /** * Sets the position of the iterator. * - * @param integer $position The new position + * @param int $position The new position * * @throws OutOfBoundsException If the position is invalid */ diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php index 391c9b1e1eafd..d4709eaea561f 100644 --- a/src/Symfony/Component/Form/FormInterface.php +++ b/src/Symfony/Component/Form/FormInterface.php @@ -94,8 +94,8 @@ public function all(); /** * Returns the errors of this form. * - * @param Boolean $deep Whether to include errors of child forms as well - * @param Boolean $flatten Whether to flatten the list of errors in case + * @param bool $deep Whether to include errors of child forms as well + * @param bool $flatten Whether to flatten the list of errors in case * $deep is set to true * * @return FormErrorIterator An iterator over the {@link FormError} diff --git a/src/Symfony/Component/HttpFoundation/JsonResponse.php b/src/Symfony/Component/HttpFoundation/JsonResponse.php index cef1c629d256d..b30afeef88c5d 100644 --- a/src/Symfony/Component/HttpFoundation/JsonResponse.php +++ b/src/Symfony/Component/HttpFoundation/JsonResponse.php @@ -117,13 +117,13 @@ public function getEncodingOptions() /** * Sets options used while encoding data to JSON. * - * @param integer $encodingOptions + * @param int $encodingOptions * * @return JsonResponse */ public function setEncodingOptions($encodingOptions) { - $this->encodingOptions = (integer) $encodingOptions; + $this->encodingOptions = (int) $encodingOptions; return $this->setData(json_decode($this->data)); } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php index ce6fbf0663d64..31b60e6e40c98 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php @@ -20,8 +20,8 @@ class ValueExporter * Converts a PHP value to a string. * * @param mixed $value The PHP value - * @param integer $depth only for internal usage - * @param Boolean $deep only for internal usage + * @param int $depth only for internal usage + * @param bool $deep only for internal usage * * @return string The string representation of the given value */ diff --git a/src/Symfony/Component/HttpKernel/Exception/UnprocessableEntityHttpException.php b/src/Symfony/Component/HttpKernel/Exception/UnprocessableEntityHttpException.php index 9516650b4ce2d..c51da532ed782 100644 --- a/src/Symfony/Component/HttpKernel/Exception/UnprocessableEntityHttpException.php +++ b/src/Symfony/Component/HttpKernel/Exception/UnprocessableEntityHttpException.php @@ -23,7 +23,7 @@ class UnprocessableEntityHttpException extends HttpException * * @param string $message The internal exception message * @param \Exception $previous The previous exception - * @param integer $code The internal exception code + * @param int $code The internal exception code */ public function __construct($message = null, \Exception $previous = null, $code = 0) { diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 19bf961a4eeda..cdf80228d8efb 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -940,13 +940,13 @@ public function isTty() /** * Sets PTY mode. * - * @param Boolean $bool + * @param bool $bool * * @return self */ public function setPty($bool) { - $this->pty = (Boolean) $bool; + $this->pty = (bool) $bool; return $this; } diff --git a/src/Symfony/Component/Process/ProcessPipes.php b/src/Symfony/Component/Process/ProcessPipes.php index 9ef6137720857..d02b767e0a135 100644 --- a/src/Symfony/Component/Process/ProcessPipes.php +++ b/src/Symfony/Component/Process/ProcessPipes.php @@ -107,7 +107,7 @@ public function closeUnixPipes() /** * Returns an array of descriptors for the use of proc_open. * - * @param Boolean $disableOutput Whether to redirect STDOUT and STDERR to /dev/null or not. + * @param bool $disableOutput Whether to redirect STDOUT and STDERR to /dev/null or not. * * @return array */ diff --git a/src/Symfony/Component/Templating/Asset/Package.php b/src/Symfony/Component/Templating/Asset/Package.php index bb7bef23f0c0a..72260ff64dbf8 100644 --- a/src/Symfony/Component/Templating/Asset/Package.php +++ b/src/Symfony/Component/Templating/Asset/Package.php @@ -57,7 +57,7 @@ public function getUrl($path, $version = null) * Applies version to the supplied path. * * @param string $path A path - * @param string|Boolean|null $version A specific version + * @param string|bool|null $version A specific version * * @return string The versionized path */ diff --git a/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php b/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php index fb4ed5dca074c..c7ac88a813dca 100644 --- a/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php +++ b/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php @@ -107,7 +107,7 @@ public function getVersion($packageName = null) * * @param string $path A public path * @param string $packageName The name of the asset package to use - * @param string|Boolean|null $version A specific version + * @param string|bool|null $version A specific version * * @return string A public path which takes into account the base path and URL path */ diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 805aa1b16b56d..b4110d7017ec6 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -109,7 +109,7 @@ public function getIterator() /** * Returns whether the given offset exists in the sequence. * - * @param integer $offset The offset + * @param int $offset The offset * * @return Boolean Whether the offset exists * @@ -124,7 +124,7 @@ public function offsetExists($offset) /** * Returns the group at the given offset. * - * @param integer $offset The offset + * @param int $offset The offset * * @return string The group a the given offset * @@ -148,7 +148,7 @@ public function offsetGet($offset) /** * Sets the group at the given offset. * - * @param integer $offset The offset + * @param int $offset The offset * @param string $value The group name * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. @@ -168,7 +168,7 @@ public function offsetSet($offset, $value) /** * Removes the group at the given offset. * - * @param integer $offset The offset + * @param int $offset The offset * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 629b0e3f5113a..e00b731eeb1c3 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -190,7 +190,7 @@ public function isPrivate($objectOrClassName) */ public function isCascaded() { - return (boolean) ($this->cascadingStrategy & CascadingStrategy::CASCADE); + return (bool) ($this->cascadingStrategy & CascadingStrategy::CASCADE); } /** @@ -204,7 +204,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return (boolean) ($this->traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE)); + return (bool) ($this->traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE)); } /** diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 0da7c7cf51104..d6c1b2500a2cc 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -264,7 +264,7 @@ protected function normalizeGroups($groups) * @param object $object The object to cascade * @param string $propertyPath The current property path * @param string[] $groups The validated groups - * @param integer $traversalStrategy The strategy for traversing the + * @param int $traversalStrategy The strategy for traversing the * cascaded object * @param ExecutionContextInterface $context The current execution context * @@ -332,7 +332,7 @@ private function validateObject($object, $propertyPath, array $groups, $traversa * @param array|\Traversable $collection The collection * @param string $propertyPath The current property path * @param string[] $groups The validated groups - * @param Boolean $stopRecursion Whether to disable + * @param bool $stopRecursion Whether to disable * recursive iteration. For * backwards compatibility * with Symfony < 2.5. @@ -416,7 +416,7 @@ private function validateEachObjectIn($collection, $propertyPath, array $groups, * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated - * @param integer $traversalStrategy The strategy used for + * @param int $traversalStrategy The strategy used for * traversing the object * @param ExecutionContextInterface $context The current execution context * @@ -608,7 +608,7 @@ private function validateClassNode($object, $cacheKey, ClassMetadataInterface $m * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated - * @param integer $traversalStrategy The strategy used for + * @param int $traversalStrategy The strategy used for * traversing the value * @param ExecutionContextInterface $context The current execution context * @@ -720,7 +720,7 @@ private function validateGenericNode($value, $object, $cacheKey, MetadataInterfa * value * @param string $propertyPath The property path leading * to the value - * @param integer $traversalStrategy The strategy used for + * @param int $traversalStrategy The strategy used for * traversing the value * @param GroupSequence $groupSequence The group sequence * @param string[]|null $cascadedGroup The group that should diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index 38cf9772d0af2..c5420c8c9fc06 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -174,7 +174,7 @@ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) /** * Sets the API version that the returned validator should support. * - * @param integer $apiVersion The required API version + * @param int $apiVersion The required API version * * @return ValidatorBuilderInterface The builder object * diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php index c522860aa75e2..84cd4d32548dd 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php @@ -82,7 +82,7 @@ public function setInvalidValue($invalidValue); * Sets the number which determines how the plural form of the violation * message is chosen when it is translated. * - * @param integer $number The number for determining the plural form + * @param int $number The number for determining the plural form * * @return ConstraintViolationBuilderInterface This builder * From 339c0c809953097d5927e7adb30a65219a269b7a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 16 Apr 2014 10:09:16 +0200 Subject: [PATCH 122/323] made {@inheritdoc} annotations consistent across the board --- src/Symfony/Bridge/Twig/Command/LintCommand.php | 2 +- src/Symfony/Component/Debug/ExceptionHandler.php | 2 +- .../EventDispatcher/Debug/TraceableEventDispatcher.php | 6 +++--- .../Component/Validator/Constraints/DateTimeValidator.php | 2 +- src/Symfony/Component/Validator/Constraints/Isbn.php | 2 +- .../Component/Validator/Constraints/UuidValidator.php | 2 +- src/Symfony/Component/Validator/Validator.php | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index 6636f6cce0e82..79cbfce22b5f2 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -32,7 +32,7 @@ class LintCommand extends Command private $twig; /** - * {@inheritDoc} + * {@inheritdoc} */ public function __construct($name = 'twig:lint') { diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index f5e8a518d399b..da1cbe6268bfe 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -57,7 +57,7 @@ public static function register($debug = true) } /** - * {@inheritDoc} + * {@inheritdoc} * * Sends a response for the given Exception. * diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php index 23ad9b77e9405..410226bb36347 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -48,7 +48,7 @@ public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $sto } /** - * {@inheritDoc} + * {@inheritdoc} */ public function addListener($eventName, $listener, $priority = 0) { @@ -122,7 +122,7 @@ public function dispatch($eventName, Event $event = null) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getCalledListeners() { @@ -138,7 +138,7 @@ public function getCalledListeners() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getNotCalledListeners() { diff --git a/src/Symfony/Component/Validator/Constraints/DateTimeValidator.php b/src/Symfony/Component/Validator/Constraints/DateTimeValidator.php index 270ff428d2f1f..7f4c3f8b9b2d2 100644 --- a/src/Symfony/Component/Validator/Constraints/DateTimeValidator.php +++ b/src/Symfony/Component/Validator/Constraints/DateTimeValidator.php @@ -24,7 +24,7 @@ class DateTimeValidator extends DateValidator const PATTERN = '/^(\d{4})-(\d{2})-(\d{2}) (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/'; /** - * {@inheritDoc} + * {@inheritdoc} */ public function validate($value, Constraint $constraint) { diff --git a/src/Symfony/Component/Validator/Constraints/Isbn.php b/src/Symfony/Component/Validator/Constraints/Isbn.php index 5beb0f05a99e6..a7626a5823c50 100644 --- a/src/Symfony/Component/Validator/Constraints/Isbn.php +++ b/src/Symfony/Component/Validator/Constraints/Isbn.php @@ -40,7 +40,7 @@ class Isbn extends Constraint public $isbn13 = false; /** - * {@inheritDoc} + * {@inheritdoc} */ public function getDefaultOption() { diff --git a/src/Symfony/Component/Validator/Constraints/UuidValidator.php b/src/Symfony/Component/Validator/Constraints/UuidValidator.php index 06d8f9c107dc7..9f1e1abb84e83 100644 --- a/src/Symfony/Component/Validator/Constraints/UuidValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UuidValidator.php @@ -49,7 +49,7 @@ class UuidValidator extends ConstraintValidator const STRICT_UUID_LENGTH = 36; /** - * {@inheritDoc} + * {@inheritdoc} */ public function validate($value, Constraint $constraint) { diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index 3b1a42fe045a1..f907287d3dedc 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -91,7 +91,7 @@ public function hasMetadataFor($value) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function validate($value, $groups = null, $traverse = false, $deep = false) { From f72eb34fc430229a7edca2f90949e1cf99dde673 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 16 Apr 2014 12:36:34 +0200 Subject: [PATCH 123/323] fixed types in phpdocs --- .../Component/ClassLoader/Psr4ClassLoader.php | 2 +- src/Symfony/Component/Console/Helper/ProgressBar.php | 12 ++++++------ .../Component/Console/Helper/QuestionHelper.php | 4 ++-- src/Symfony/Component/Console/Helper/Table.php | 6 +++--- src/Symfony/Component/Console/Question/Question.php | 6 +++--- .../Core/EventListener/ResizeFormListener.php | 2 +- src/Symfony/Component/Form/FormErrorIterator.php | 10 +++++----- .../Component/HttpFoundation/JsonResponse.php | 2 +- src/Symfony/Component/Process/Process.php | 6 +++--- src/Symfony/Component/Process/ProcessPipes.php | 2 +- .../Component/PropertyAccess/PropertyAccessor.php | 2 +- .../PropertyAccess/PropertyAccessorInterface.php | 4 ++-- src/Symfony/Component/Routing/Route.php | 2 +- .../Security/Acl/Permission/MaskBuilder.php | 2 +- .../Validator/Constraints/EmailValidator.php | 2 +- .../Validator/Constraints/GroupSequence.php | 4 ++-- src/Symfony/Component/Validator/Constraints/Isbn.php | 4 ++-- .../Validator/Context/ExecutionContextInterface.php | 4 ++-- .../Component/Validator/Mapping/ClassMetadata.php | 4 ++-- .../Validator/Mapping/ClassMetadataInterface.php | 4 ++-- .../Mapping/Factory/LazyLoadingMetadataFactory.php | 2 +- .../Component/Validator/Mapping/GenericMetadata.php | 6 +++--- .../Validator/Mapping/MetadataInterface.php | 4 ++-- src/Symfony/Component/Validator/ValidatorBuilder.php | 2 +- .../Violation/ConstraintViolationBuilder.php | 2 +- 25 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php b/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php index 60aa80279b582..429812d1156a4 100644 --- a/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php +++ b/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php @@ -60,7 +60,7 @@ public function findFile($class) /** * @param string $class * - * @return Boolean + * @return bool */ public function loadClass($class) { diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 4790270bd474e..ff101ca5dbd1c 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -150,7 +150,7 @@ public function getMessage($name = 'message') /** * Gets the progress bar start time. * - * @return integer The progress bar start time + * @return int The progress bar start time */ public function getStartTime() { @@ -160,7 +160,7 @@ public function getStartTime() /** * Gets the progress bar maximal steps. * - * @return integer The progress bar max steps + * @return int The progress bar max steps */ public function getMaxSteps() { @@ -170,7 +170,7 @@ public function getMaxSteps() /** * Gets the progress bar step. * - * @return integer The progress bar step + * @return int The progress bar step */ public function getStep() { @@ -180,7 +180,7 @@ public function getStep() /** * Gets the progress bar step width. * - * @return integer The progress bar step width + * @return int The progress bar step width */ public function getStepWidth() { @@ -190,7 +190,7 @@ public function getStepWidth() /** * Gets the current progress bar percent. * - * @return integer The current progress bar percent + * @return int The current progress bar percent */ public function getProgressPercent() { @@ -210,7 +210,7 @@ public function setBarWidth($size) /** * Gets the progress bar width. * - * @return integer The progress bar size + * @return int The progress bar size */ public function getBarWidth() { diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 57dc5db45e81f..a77c052a44b26 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -372,7 +372,7 @@ private function validateAttempts($interviewer, OutputInterface $output, Questio /** * Returns a valid unix shell. * - * @return string|Boolean The valid shell name, false in case no valid shell is found + * @return string|bool The valid shell name, false in case no valid shell is found */ private function getShell() { @@ -399,7 +399,7 @@ private function getShell() /** * Returns whether Stty is available or not. * - * @return Boolean + * @return bool */ private function hasSttyAvailable() { diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index f3cd7ce8e078d..5bf7f55465e2f 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -318,7 +318,7 @@ private function renderCell(array $row, $column, $cellFormat) /** * Gets number of columns for this table. * - * @return integer + * @return int */ private function getNumberOfColumns() { @@ -339,7 +339,7 @@ private function getNumberOfColumns() * * @param int $column * - * @return integer + * @return int */ private function getColumnWidth($column) { @@ -365,7 +365,7 @@ private function getColumnWidth($column) * @param array $row * @param int $column * - * @return integer + * @return int */ private function getCellWidth(array $row, $column) { diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index 2fd8283f3ae69..e47bfb1e9249c 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -63,7 +63,7 @@ public function getDefault() /** * Returns whether the user response must be hidden. * - * @return Boolean + * @return bool */ public function isHidden() { @@ -93,7 +93,7 @@ public function setHidden($hidden) /** * In case the response can not be hidden, whether to fallback on non-hidden question or not. * - * @return Boolean + * @return bool */ public function isHiddenFallback() { @@ -202,7 +202,7 @@ public function setMaxAttempts($attempts) * * Null means an unlimited number of attempts. * - * @return null|integer + * @return null|int */ public function getMaxAttempts() { diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php index 6f2be3c44dda4..e88db694024ca 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php @@ -46,7 +46,7 @@ class ResizeFormListener implements EventSubscriberInterface protected $allowDelete; /** - * @var Boolean + * @var bool */ private $deleteEmpty; diff --git a/src/Symfony/Component/Form/FormErrorIterator.php b/src/Symfony/Component/Form/FormErrorIterator.php index 83d7aefc367c0..58a94f815aab9 100644 --- a/src/Symfony/Component/Form/FormErrorIterator.php +++ b/src/Symfony/Component/Form/FormErrorIterator.php @@ -128,7 +128,7 @@ public function next() /** * Returns the current position of the iterator. * - * @return integer The 0-indexed position. + * @return int The 0-indexed position. */ public function key() { @@ -138,7 +138,7 @@ public function key() /** * Returns whether the iterator's position is valid. * - * @return Boolean Whether the iterator is valid. + * @return bool Whether the iterator is valid. */ public function valid() { @@ -161,7 +161,7 @@ public function rewind() * * @param int $position The position * - * @return Boolean Whether that position exists + * @return bool Whether that position exists */ public function offsetExists($position) { @@ -210,7 +210,7 @@ public function offsetUnset($position) * Returns whether the current element of the iterator can be recursed * into. * - * @return Boolean Whether the current element is an instance of this class + * @return bool Whether the current element is an instance of this class */ public function hasChildren() { @@ -240,7 +240,7 @@ public function getChildren() * * $count = count($form->getErrors(true, true)); * - * @return integer The number of iterated elements + * @return int The number of iterated elements */ public function count() { diff --git a/src/Symfony/Component/HttpFoundation/JsonResponse.php b/src/Symfony/Component/HttpFoundation/JsonResponse.php index b30afeef88c5d..2e40d403f37a8 100644 --- a/src/Symfony/Component/HttpFoundation/JsonResponse.php +++ b/src/Symfony/Component/HttpFoundation/JsonResponse.php @@ -107,7 +107,7 @@ public function setData($data = array()) /** * Returns options used while encoding data to JSON. * - * @return integer + * @return int */ public function getEncodingOptions() { diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 994f9cc58b941..49bad7478e789 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -441,7 +441,7 @@ public function enableOutput() /** * Returns true in case the output is disabled, false otherwise. * - * @return Boolean + * @return bool */ public function isOutputDisabled() { @@ -954,7 +954,7 @@ public function setPty($bool) /** * Returns PTY state. * - * @return Boolean + * @return bool */ public function isPty() { @@ -1161,7 +1161,7 @@ public function checkTimeout() /** * Returns whether PTY is supported on the current operating system. * - * @return Boolean + * @return bool */ public static function isPtySupported() { diff --git a/src/Symfony/Component/Process/ProcessPipes.php b/src/Symfony/Component/Process/ProcessPipes.php index bb47a181eb1c6..82bf5c25779e7 100644 --- a/src/Symfony/Component/Process/ProcessPipes.php +++ b/src/Symfony/Component/Process/ProcessPipes.php @@ -30,7 +30,7 @@ class ProcessPipes private $useFiles; /** @var bool */ private $ttyMode; - /** @var Boolean */ + /** @var bool */ private $ptyMode; const CHUNK_SIZE = 16384; diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 64ef65d883f38..897b68e7484a3 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -497,7 +497,7 @@ private function writeCollection($object, $property, $collection, $addMethod, $r * @param object $object The object to write to * @param string $property The property to write * - * @return Boolean Whether the property is writable + * @return bool Whether the property is writable */ private function isPropertyWritable($object, $property) { diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php index 42e0d1e95d1f8..da78dc692861c 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php @@ -92,7 +92,7 @@ public function getValue($objectOrArray, $propertyPath); * @param object|array $objectOrArray The object or array to check * @param string|PropertyPathInterface $propertyPath The property path to check * - * @return Boolean Whether the value can be set + * @return bool Whether the value can be set * * @throws Exception\InvalidArgumentException If the property path is invalid */ @@ -107,7 +107,7 @@ public function isWritable($objectOrArray, $propertyPath); * @param object|array $objectOrArray The object or array to check * @param string|PropertyPathInterface $propertyPath The property path to check * - * @return Boolean Whether the property path can be read + * @return bool Whether the property path can be read * * @throws Exception\InvalidArgumentException If the property path is invalid */ diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 04f743b69bf5d..ad8ec49b206cc 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -252,7 +252,7 @@ public function setSchemes($schemes) * * @param string $scheme * - * @return Boolean true if the scheme requirement exists, otherwise false + * @return bool true if the scheme requirement exists, otherwise false */ public function hasScheme($scheme) { diff --git a/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php b/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php index b99dfb26cff05..6b00b27f0fff1 100644 --- a/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php +++ b/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php @@ -198,7 +198,7 @@ public static function getCode($mask) * * @param mixed $code * - * @return integer + * @return int * * @throws \InvalidArgumentException */ diff --git a/src/Symfony/Component/Validator/Constraints/EmailValidator.php b/src/Symfony/Component/Validator/Constraints/EmailValidator.php index d925abcb0da05..8263577f7f907 100644 --- a/src/Symfony/Component/Validator/Constraints/EmailValidator.php +++ b/src/Symfony/Component/Validator/Constraints/EmailValidator.php @@ -26,7 +26,7 @@ class EmailValidator extends ConstraintValidator /** * isStrict * - * @var Boolean + * @var bool */ private $isStrict; diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index b4110d7017ec6..af476a3c6014b 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -111,7 +111,7 @@ public function getIterator() * * @param int $offset The offset * - * @return Boolean Whether the offset exists + * @return bool Whether the offset exists * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. @@ -181,7 +181,7 @@ public function offsetUnset($offset) /** * Returns the number of groups in the sequence. * - * @return integer The number of groups + * @return int The number of groups * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. diff --git a/src/Symfony/Component/Validator/Constraints/Isbn.php b/src/Symfony/Component/Validator/Constraints/Isbn.php index a7626a5823c50..90168bceef4c1 100644 --- a/src/Symfony/Component/Validator/Constraints/Isbn.php +++ b/src/Symfony/Component/Validator/Constraints/Isbn.php @@ -29,13 +29,13 @@ class Isbn extends Constraint /** * @deprecated Deprecated since version 2.5, to be removed in 3.0. Use option "type" instead. - * @var Boolean + * @var bool */ public $isbn10 = false; /** * @deprecated Deprecated since version 2.5, to be removed in 3.0. Use option "type" instead. - * @var Boolean + * @var bool */ public $isbn13 = false; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index ab539fd81de42..37518fdf7a680 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -155,7 +155,7 @@ public function markGroupAsValidated($cacheKey, $groupHash); * @param string $groupHash The group's name or hash, if it is group * sequence * - * @return Boolean Whether the object was already validated for that + * @return bool Whether the object was already validated for that * group * * @internal Used by the validator engine. Should not be called by user @@ -180,7 +180,7 @@ public function markConstraintAsValidated($cacheKey, $constraintHash); * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * - * @return Boolean Whether the constraint was already validated + * @return bool Whether the constraint was already validated * * @internal Used by the validator engine. Should not be called by user * code. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 643af595864a2..ffba37a568818 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -99,7 +99,7 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, * * By default, only instances of {@link \Traversable} are traversed. * - * @var integer + * @var int * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use @@ -491,7 +491,7 @@ public function isGroupSequenceProvider() /** * Class nodes are never cascaded. * - * @return Boolean Always returns false. + * @return bool Always returns false. */ public function getCascadingStrategy() { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index 332f5fa1c86cb..bf1e36f666e90 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -46,7 +46,7 @@ public function getConstrainedProperties(); * * If it is, you can access the group sequence with {@link getGroupSequence()}. * - * @return Boolean Returns true if the "Default" group is overridden + * @return bool Returns true if the "Default" group is overridden * * @see \Symfony\Component\Validator\Constraints\GroupSequence */ @@ -71,7 +71,7 @@ public function getGroupSequence(); * This interface will be used to obtain the group sequence when an object * of this class is validated. * - * @return Boolean Returns true if the "Default" group is overridden by + * @return bool Returns true if the "Default" group is overridden by * a dynamic group sequence * * @see \Symfony\Component\Validator\GroupSequenceProviderInterface diff --git a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php index 240b6be29997f..eb0f3c460e09f 100644 --- a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php @@ -146,7 +146,7 @@ public function getMetadataFor($value) * * @param string|object $value A class name or an object * - * @return Boolean Whether metadata can be returned for that class + * @return bool Whether metadata can be returned for that class */ public function hasMetadataFor($value) { diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 33b40fbd369c6..01a6c3226ac06 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -51,7 +51,7 @@ class GenericMetadata implements MetadataInterface * * By default, objects are not cascaded. * - * @var integer + * @var int * * @see CascadingStrategy * @@ -66,7 +66,7 @@ class GenericMetadata implements MetadataInterface * * By default, traversable objects are not traversed. * - * @var integer + * @var int * * @see TraversalStrategy * @@ -192,7 +192,7 @@ public function getConstraints() /** * Returns whether this element has any constraints. * - * @return Boolean + * @return bool */ public function hasConstraints() { diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index e947c8dfe35a5..a72d4a5801c92 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -34,7 +34,7 @@ interface MetadataInterface extends LegacyMetadataInterface /** * Returns the strategy for cascading objects. * - * @return integer The cascading strategy + * @return int The cascading strategy * * @see CascadingStrategy */ @@ -43,7 +43,7 @@ public function getCascadingStrategy(); /** * Returns the strategy for traversing traversable objects. * - * @return integer The traversal strategy + * @return int The traversal strategy * * @see TraversalStrategy */ diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 6f33a8d222a00..896eaecbdfeaf 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -97,7 +97,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface private $propertyAccessor; /** - * @var integer + * @var int */ private $apiVersion; diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php index d5905a0a5cbcb..969b79cdeddd5 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -68,7 +68,7 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface private $translationDomain; /** - * @var integer|null + * @var int|null */ private $plural; From 469943cd1c6994eace522a35cf5ee93348884c77 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 17 Apr 2014 09:51:05 +0200 Subject: [PATCH 124/323] [HttpFoundation] factorize out the way output buffers should be closed --- .../Controller/ExceptionController.php | 14 ++---- .../Component/HttpFoundation/Response.php | 50 ++++++++++++------- .../Fragment/InlineFragmentRenderer.php | 4 +- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index 9557fb4aba92c..e0acb565648fb 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -70,17 +70,13 @@ public function showAction(Request $request, FlattenException $exception, DebugL */ protected function getAndCleanOutputBuffering($startObLevel) { - // ob_get_level() never returns 0 on some Windows configurations, so if - // the level is the same two times in a row, the loop should be stopped. - $previousObLevel = null; - $currentContent = ''; - - while (($obLevel = ob_get_level()) > $startObLevel && $obLevel !== $previousObLevel) { - $previousObLevel = $obLevel; - $currentContent .= ob_get_clean(); + if (ob_get_level() <= $startObLevel) { + return ''; } - return $currentContent; + Response::closeOutputBuffers($startObLevel + 1, true); + + return ob_get_clean(); } /** diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 9ceeb2370253f..0b7ec3d826916 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -376,24 +376,7 @@ public function send() if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } elseif ('cli' !== PHP_SAPI) { - // ob_get_level() never returns 0 on some Windows configurations, so if - // the level is the same two times in a row, the loop should be stopped. - $previous = null; - $obStatus = ob_get_status(1); - while (($level = ob_get_level()) > 0 && $level !== $previous) { - $previous = $level; - if ($obStatus[$level - 1]) { - if (version_compare(PHP_VERSION, '5.4', '>=')) { - if (isset($obStatus[$level - 1]['flags']) && ($obStatus[$level - 1]['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE)) { - ob_end_flush(); - } - } else { - if (isset($obStatus[$level - 1]['del']) && $obStatus[$level - 1]['del']) { - ob_end_flush(); - } - } - } - } + static::closeOutputBuffers(0, true); flush(); } @@ -1247,7 +1230,36 @@ public function isEmpty() } /** - * Check if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9 + * Cleans or flushes output buffers up to target level. + * + * Resulting level can be greater than target level if a non-removable buffer has been encountered. + * + * @param int $targetLevel The target output buffering level + * @param bool $flush Whether to flush or clean the buffers + */ + public static function closeOutputBuffers($targetLevel, $flush) + { + $status = ob_get_status(true); + $level = count($status); + + while ($level-- > $targetLevel + && (!empty($status[$level]['del']) + || (isset($status[$level]['flags']) + && ($status[$level]['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE) + && ($status[$level]['flags'] & ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE)) + ) + ) + ) { + if ($flush) { + ob_end_flush(); + } else { + ob_end_clean(); + } + } + } + + /** + * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9 * * @link http://support.microsoft.com/kb/323308 */ diff --git a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php index c6ca3d475e882..a6ab82ea28efa 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php @@ -93,9 +93,7 @@ public function render($uri, Request $request, array $options = array()) } // let's clean up the output buffers that were created by the sub-request - while (ob_get_level() > $level) { - ob_get_clean(); - } + Response::closeOutputBuffers($level, false); if (isset($options['alt'])) { $alt = $options['alt']; From e0433297b28283686fd5b4475c4649cffb93e98b Mon Sep 17 00:00:00 2001 From: ibasaw Date: Fri, 18 Apr 2014 13:56:04 +0200 Subject: [PATCH 125/323] Update PackageInterface.php --- src/Symfony/Component/Templating/Asset/PackageInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Templating/Asset/PackageInterface.php b/src/Symfony/Component/Templating/Asset/PackageInterface.php index 528101e3ca525..84fa3c0b753eb 100644 --- a/src/Symfony/Component/Templating/Asset/PackageInterface.php +++ b/src/Symfony/Component/Templating/Asset/PackageInterface.php @@ -29,7 +29,7 @@ public function getVersion(); * Returns an absolute or root-relative public path. * * @param string $path A path - * @prama string|Boolean|null $version A specific version for the path + * @param string|Boolean|null $version A specific version for the path * * @return string The public path */ From 3c423452933b8cae2fd6f7f53daf16d5af62518b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 18 Apr 2014 21:44:02 +0200 Subject: [PATCH 126/323] fixed typo --- src/Symfony/Component/Templating/Asset/PackageInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Templating/Asset/PackageInterface.php b/src/Symfony/Component/Templating/Asset/PackageInterface.php index 84fa3c0b753eb..7317b555ac476 100644 --- a/src/Symfony/Component/Templating/Asset/PackageInterface.php +++ b/src/Symfony/Component/Templating/Asset/PackageInterface.php @@ -28,8 +28,8 @@ public function getVersion(); /** * Returns an absolute or root-relative public path. * - * @param string $path A path - * @param string|Boolean|null $version A specific version for the path + * @param string $path A path + * @param string|bool|null $version A specific version for the path * * @return string The public path */ From 8ef8a1d289a6ce454b7c79baeddbfb45e4af6191 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 18 Apr 2014 22:40:52 +0200 Subject: [PATCH 127/323] unified return null usages --- .../PropertyAccess/Tests/Fixtures/TestClassMagicCall.php | 2 -- .../PropertyAccess/Tests/Fixtures/TestClassMagicGet.php | 2 -- src/Symfony/Component/Serializer/Serializer.php | 4 ---- 3 files changed, 8 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php index d49967abd1a66..0d6c1f0ba97d9 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicCall.php @@ -33,7 +33,5 @@ public function __call($method, array $args) if ('setMagicCallProperty' === $method) { $this->magicCallProperty = reset($args); } - - return null; } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php index fee8318966728..2f325aa6a729e 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassMagicGet.php @@ -36,7 +36,5 @@ public function __get($property) if ('constantMagicProperty' === $property) { return 'constant value'; } - - return null; } } diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 76483d8dd8028..55fa0cd1ca68d 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -177,8 +177,6 @@ private function getNormalizer($data, $format) return $normalizer; } } - - return null; } /** @@ -203,8 +201,6 @@ private function getDenormalizer($data, $class, $format) return $normalizer; } } - - return null; } /** From 90294ec90c85dacb4a7201c2e2503936352ac70e Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Fri, 18 Apr 2014 22:54:24 +0200 Subject: [PATCH 128/323] Fix doc blocks --- .../Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php | 5 ++++- src/Symfony/Component/Console/Helper/TableStyle.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php index 7abe79a5df5c6..7e6f10fe05d55 100644 --- a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php +++ b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php @@ -79,8 +79,11 @@ class ModelChoiceList extends ObjectChoiceList * @param array|ModelCriteria $preferred The preferred items of this choice. * Either an array if $choices is given, * or a ModelCriteria to be merged with the $queryObject. - * @param string $useAsIdentifier a custome unique column (eg slug) to use instead of primary key * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. + * @param string $useAsIdentifier a custom unique column (eg slug) to use instead of primary key. + * + * @throws MissingOptionsException In case the class parameter is empty. + * @throws InvalidOptionsException In case the query class is not found. */ public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array(), PropertyAccessorInterface $propertyAccessor = null, $useAsIdentifier = null) { diff --git a/src/Symfony/Component/Console/Helper/TableStyle.php b/src/Symfony/Component/Console/Helper/TableStyle.php index 2379d9b1afa8c..338f1a060af66 100644 --- a/src/Symfony/Component/Console/Helper/TableStyle.php +++ b/src/Symfony/Component/Console/Helper/TableStyle.php @@ -242,7 +242,7 @@ public function setPadType($padType) /** * Gets cell padding type. * - * @param int + * @return int */ public function getPadType() { From 0279fbfdadc71f7940e41fdf41ee1d34792fd616 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 17 Apr 2014 09:44:42 +0200 Subject: [PATCH 129/323] [Debug] Handled errors --- .../Resources/config/debug.xml | 9 ++ .../Bundle/FrameworkBundle/composer.json | 2 +- src/Symfony/Component/Debug/CHANGELOG.md | 5 ++ src/Symfony/Component/Debug/ErrorHandler.php | 62 ++++++++----- .../Exception/ClassNotFoundException.php | 1 + .../Debug/Exception/ContextErrorException.php | 19 +--- .../Debug/Exception/DummyException.php | 4 +- .../Debug/Exception/FatalErrorException.php | 48 ++++++++++- .../Debug/Exception/FlattenException.php | 31 +------ .../Debug/Exception/HandledErrorException.php | 86 +++++++++++++++++++ .../Exception/UndefinedFunctionException.php | 1 + .../Exception/UndefinedMethodException.php | 10 ++- .../Component/Debug/ExceptionHandler.php | 4 +- .../Debug/ExceptionHandlerInterface.php | 2 + .../Debug/Tests/DebugClassLoaderTest.php | 12 +-- .../Debug/Tests/ErrorHandlerTest.php | 8 +- .../DataCollector/LoggerDataCollector.php | 2 +- .../EventListener/ErrorsLoggerListener.php | 2 +- .../FatalErrorExceptionsListener.php | 45 ++++++++++ .../Fragment/InlineFragmentRenderer.php | 12 ++- .../Component/HttpKernel/HttpKernel.php | 20 +++++ .../Component/HttpKernel/composer.json | 2 +- 22 files changed, 297 insertions(+), 90 deletions(-) create mode 100644 src/Symfony/Component/Debug/Exception/HandledErrorException.php create mode 100644 src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index 875c556574be8..2366ac1f0604e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -9,6 +9,7 @@ Symfony\Component\Stopwatch\Stopwatch %kernel.cache_dir%/%kernel.container_class%.xml Symfony\Component\HttpKernel\Controller\TraceableControllerResolver + Symfony\Component\HttpKernel\EventListener\FatalErrorExceptionsListener @@ -39,5 +40,13 @@ scream + + + + + + handleFatalErrorException + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index e1ec07d3989f1..894f1f7869c32 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -21,7 +21,7 @@ "symfony/config" : "~2.4", "symfony/event-dispatcher": "~2.5", "symfony/http-foundation": "~2.4", - "symfony/http-kernel": "~2.4", + "symfony/http-kernel": "~2.5", "symfony/filesystem": "~2.3", "symfony/routing": "~2.2", "symfony/security-core": "~2.4", diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index d20fde043b7ab..3823fc8783563 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -4,7 +4,12 @@ CHANGELOG 2.5.0 ----- +* added HandledErrorException +* added ErrorHandler::setFatalErrorExceptionHandler() * added UndefinedMethodFatalErrorHandler +* deprecated ExceptionHandlerInterface +* deprecated ContextErrorException +* deprecated DummyException 2.4.0 ----- diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index f90ba426ff670..46296f45e7fd5 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -13,9 +13,9 @@ use Psr\Log\LogLevel; use Psr\Log\LoggerInterface; -use Symfony\Component\Debug\Exception\ContextErrorException; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\DummyException; +use Symfony\Component\Debug\Exception\HandledErrorException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; @@ -63,6 +63,8 @@ class ErrorHandler private static $stackedErrorLevels = array(); + private static $fatalHandler = false; + /** * Registers the error handler. * @@ -117,7 +119,17 @@ public static function setLogger(LoggerInterface $logger, $channel = 'deprecatio } /** - * @throws ContextErrorException When error_reporting returns error + * Sets a fatal error exception handler. + * + * @param callable $handler An handler that will be called on FatalErrorException + */ + public static function setFatalErrorExceptionHandler($handler) + { + self::$fatalHandler = $handler; + } + + /** + * @throws HandledErrorException When error_reporting returns error */ public function handle($level, $message, $file = 'unknown', $line = 0, $context = array()) { @@ -152,7 +164,7 @@ function ($row) { $exceptionHandler = set_exception_handler('var_dump'); restore_exception_handler(); - if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandlerInterface) { + if ($exceptionHandler) { if (self::$stackedErrorLevels) { self::$stackedErrors[] = func_get_args(); @@ -160,22 +172,22 @@ function ($row) { } $exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line); - $exception = new ContextErrorException($exception, 0, $level, $file, $line, $context); - $exceptionHandler[0]->handle($exception); + $exception = new HandledErrorException($exception, 0, $level, $file, $line, $context); + $exception->handleWith($exceptionHandler); // we must stop the PHP script execution, as the exception has // already been dealt with, so, let's throw an exception that // will be caught by a dummy exception handler set_exception_handler(function (\Exception $e) use ($exceptionHandler) { - if (!$e instanceof DummyException) { - // happens if our dummy exception is caught by a - // catch-all from user code, in which case, let's the + if (!$e instanceof HandledErrorException && !$e instanceof DummyException) { + // happens if our handled exception is caught by a + // catch-all from user code, in which case, let the // current handler handle this "new" exception call_user_func($exceptionHandler, $e); } }); - throw new DummyException(); + throw $exception; } } @@ -256,6 +268,7 @@ public static function unstackErrors() public function handleFatal() { $this->reservedMemory = ''; + gc_collect_cycles(); $error = error_get_last(); while (self::$stackedErrorLevels) { @@ -281,16 +294,14 @@ public function handleFatal() self::$loggers['emergency']->emergency($error['message'], $fatal); } - if (!$this->displayErrors) { - return; - } - - // get current exception handler - $exceptionHandler = set_exception_handler('var_dump'); - restore_exception_handler(); + if ($this->displayErrors) { + // get current exception handler + $exceptionHandler = set_exception_handler('var_dump'); + restore_exception_handler(); - if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandlerInterface) { - $this->handleFatalError($exceptionHandler[0], $error); + if ($exceptionHandler || self::$fatalHandler) { + $this->handleFatalError($exceptionHandler, $error); + } } } @@ -310,18 +321,25 @@ protected function getFatalErrorHandlers() ); } - private function handleFatalError(ExceptionHandlerInterface $exceptionHandler, array $error) + private function handleFatalError($exceptionHandler, array $error) { $level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type']; $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); - $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']); + $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line'], 3); foreach ($this->getFatalErrorHandlers() as $handler) { if ($ex = $handler->handleError($error, $exception)) { - return $exceptionHandler->handle($ex); + $exception = $ex; + break; } } - $exceptionHandler->handle($exception); + if ($exceptionHandler) { + $exception->handleWith($exceptionHandler); + } + + if (self::$fatalHandler) { + call_user_func(self::$fatalHandler, $exception); + } } } diff --git a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php index 94169b45a2075..b91bf46631bbb 100644 --- a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -28,5 +28,6 @@ public function __construct($message, \ErrorException $previous) $previous->getLine(), $previous->getPrevious() ); + $this->setTrace($previous->getTrace()); } } diff --git a/src/Symfony/Component/Debug/Exception/ContextErrorException.php b/src/Symfony/Component/Debug/Exception/ContextErrorException.php index 54f0198f1b2f8..d6a9eaf3da2b2 100644 --- a/src/Symfony/Component/Debug/Exception/ContextErrorException.php +++ b/src/Symfony/Component/Debug/Exception/ContextErrorException.php @@ -15,22 +15,9 @@ * Error Exception with Variable Context. * * @author Christian Sciberras + * + * @deprecated since version 2.5, to be removed in 3.0. */ -class ContextErrorException extends \ErrorException +class ContextErrorException extends HandledErrorException { - private $context = array(); - - public function __construct($message, $code, $severity, $filename, $lineno, $context = array()) - { - parent::__construct($message, $code, $severity, $filename, $lineno); - $this->context = $context; - } - - /** - * @return array Array of variables that existed when the exception occurred - */ - public function getContext() - { - return $this->context; - } } diff --git a/src/Symfony/Component/Debug/Exception/DummyException.php b/src/Symfony/Component/Debug/Exception/DummyException.php index 8891f2fed6ebd..967e033777322 100644 --- a/src/Symfony/Component/Debug/Exception/DummyException.php +++ b/src/Symfony/Component/Debug/Exception/DummyException.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Debug\Exception; /** - * Used to stop execution of a PHP script after handling a fatal error. - * * @author Fabien Potencier + * + * @deprecated since version 2.5, to be removed in 3.0. */ class DummyException extends \ErrorException { diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index bf37ef809857f..47375a370079f 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -14,8 +14,54 @@ /** * Fatal Error Exception. * + * @author Fabien Potencier * @author Konstanton Myakshin + * @author Nicolas Grekas */ -class FatalErrorException extends \ErrorException +class FatalErrorException extends HandledErrorException { + public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null) + { + parent::__construct($message, $code, $severity, $filename, $lineno); + + if (null !== $traceOffset) { + if (function_exists('xdebug_get_function_stack')) { + $trace = xdebug_get_function_stack(); + if (0 < $traceOffset) { + $trace = array_slice($trace, 0, -$traceOffset); + } + $trace = array_reverse($trace); + + foreach ($trace as $i => $frame) { + if (!isset($frame['type'])) { + // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 + if (isset($frame['class'])) { + $trace[$i]['type'] = '::'; + } + } elseif ('dynamic' === $frame['type']) { + $trace[$i]['type'] = '->'; + } elseif ('static' === $frame['type']) { + $trace[$i]['type'] = '::'; + } + + // XDebug also has a different name for the parameters array + if (isset($frame['params']) && !isset($frame['args'])) { + $trace[$i]['args'] = $frame['params']; + unset($trace[$i]['params']); + } + } + } else { + $trace = array(); + } + + $this->setTrace($trace); + } + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } } diff --git a/src/Symfony/Component/Debug/Exception/FlattenException.php b/src/Symfony/Component/Debug/Exception/FlattenException.php index 878ac4d7740e8..eb49d4609fdf6 100644 --- a/src/Symfony/Component/Debug/Exception/FlattenException.php +++ b/src/Symfony/Component/Debug/Exception/FlattenException.php @@ -172,36 +172,7 @@ public function getTrace() public function setTraceFromException(\Exception $exception) { - $trace = $exception->getTrace(); - - if ($exception instanceof FatalErrorException) { - if (function_exists('xdebug_get_function_stack')) { - $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4); - - foreach ($trace as $i => $frame) { - // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 - if (!isset($frame['type'])) { - $trace[$i]['type'] = '??'; - } - - if ('dynamic' === $trace[$i]['type']) { - $trace[$i]['type'] = '->'; - } elseif ('static' === $trace[$i]['type']) { - $trace[$i]['type'] = '::'; - } - - // XDebug also has a different name for the parameters array - if (isset($frame['params']) && !isset($frame['args'])) { - $trace[$i]['args'] = $frame['params']; - unset($trace[$i]['params']); - } - } - } else { - $trace = array_slice(array_reverse($trace), 1); - } - } - - $this->setTrace($trace, $exception->getFile(), $exception->getLine()); + $this->setTrace($exception->getTrace(), $exception->getFile(), $exception->getLine()); } public function setTrace($trace, $file, $line) diff --git a/src/Symfony/Component/Debug/Exception/HandledErrorException.php b/src/Symfony/Component/Debug/Exception/HandledErrorException.php new file mode 100644 index 0000000000000..83219d119689c --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/HandledErrorException.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Exception; + +/** + * @author Nicolas Grekas + */ +class HandledErrorException extends \ErrorException +{ + private $handlerOutput = false; + private $context = array(); + + public function __construct($message, $code, $severity, $filename, $lineno, $context = array()) + { + parent::__construct($message, $code, $severity, $filename, $lineno); + $this->context = $context; + } + + /** + * @return array Array of variables that existed when the exception occurred + */ + public function getContext() + { + return $this->context; + } + + public function handleWith($exceptionHandler) + { + $this->handlerOutput = false; + ob_start(array($this, 'catchOutput')); + call_user_func($exceptionHandler, $this); + if (false === $this->handlerOutput) { + ob_end_clean(); + } + ob_start(array(__CLASS__, 'flushOutput')); + echo $this->handlerOutput; + $this->handlerOutput = ob_get_length(); + } + + /** + * @internal + */ + public function catchOutput($buffer) + { + $this->handlerOutput = $buffer; + + return ''; + } + + /** + * @internal + */ + public static function flushOutput($buffer) + { + return $buffer; + } + + public function cleanOutput() + { + $status = ob_get_status(); + + if (isset($status['name']) && __CLASS__.'::flushOutput' === $status['name']) { + if ($this->handlerOutput) { + // use substr_replace() instead of substr() for mbstring overloading resistance + echo substr_replace(ob_get_clean(), '', 0, $this->handlerOutput); + } else { + ob_end_flush(); + } + } + } + + public function __destruct() + { + $this->handlerOutput = 0; + $this->cleanOutput(); + } +} diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php index 572c8b30a1a7b..a66ae2a3879c9 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -28,5 +28,6 @@ public function __construct($message, \ErrorException $previous) $previous->getLine(), $previous->getPrevious() ); + $this->setTrace($previous->getTrace()); } } diff --git a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php index d84031b4429e9..350dc3187f475 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php @@ -20,6 +20,14 @@ class UndefinedMethodException extends FatalErrorException { public function __construct($message, \ErrorException $previous) { - parent::__construct($message, $previous->getCode(), $previous->getSeverity(), $previous->getFile(), $previous->getLine(), $previous->getPrevious()); + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); } } diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index da1cbe6268bfe..91e904fbe25ce 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -71,7 +71,9 @@ public static function register($debug = true) public function handle(\Exception $exception) { if (class_exists('Symfony\Component\HttpFoundation\Response')) { - $this->createResponse($exception)->send(); + $response = $this->createResponse($exception); + $response->sendHeaders(); + $response->sendContent(); } else { $this->sendPhpResponse($exception); } diff --git a/src/Symfony/Component/Debug/ExceptionHandlerInterface.php b/src/Symfony/Component/Debug/ExceptionHandlerInterface.php index 5ebefcf829e3f..f1740184c6dfe 100644 --- a/src/Symfony/Component/Debug/ExceptionHandlerInterface.php +++ b/src/Symfony/Component/Debug/ExceptionHandlerInterface.php @@ -15,6 +15,8 @@ * An ExceptionHandler does something useful with an exception. * * @author Andrew Moore + * + * @deprecated since version 2.5, to be removed in 3.0. */ interface ExceptionHandlerInterface { diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 0c071414f7723..fd98a5116e1fd 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -78,14 +78,14 @@ public function testUnsilencing() } /** - * @expectedException \Symfony\Component\Debug\Exception\DummyException + * @expectedException \Symfony\Component\Debug\Exception\HandledErrorException */ public function testStacking() { - // the ContextErrorException must not be loaded to test the workaround + // the HandledErrorException must not be loaded to test the workaround // for https://bugs.php.net/65322. - if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) { - $this->markTestSkipped('The ContextErrorException class is already loaded.'); + if (class_exists('Symfony\Component\Debug\Exception\HandledErrorException', false)) { + $this->markTestSkipped('The HandledErrorException class is already loaded.'); } $exceptionHandler = $this->getMock('Symfony\Component\Debug\ExceptionHandler', array('handle')); @@ -93,7 +93,7 @@ public function testStacking() $that = $this; $exceptionCheck = function ($exception) use ($that) { - $that->assertInstanceOf('Symfony\Component\Debug\Exception\ContextErrorException', $exception); + $that->assertInstanceOf('Symfony\Component\Debug\Exception\HandledErrorException', $exception); $that->assertEquals(E_STRICT, $exception->getSeverity()); $that->assertStringStartsWith(__FILE__, $exception->getFile()); $that->assertRegexp('/^Runtime Notice: Declaration/', $exception->getMessage()); @@ -107,7 +107,7 @@ public function testStacking() try { // Trigger autoloading + E_STRICT at compile time // which in turn triggers $errorHandler->handle() - // that again triggers autoloading for ContextErrorException. + // that again triggers autoloading for HandledErrorException. // Error stacking works around the bug above and everything is fine. eval(' diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 133dfa7af305a..7786c14ef13a6 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Debug\Tests; use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\Exception\DummyException; +use Symfony\Component\Debug\Exception\HandledErrorException; /** * ErrorHandlerTest @@ -51,7 +51,7 @@ public function testNotice() $that = $this; $exceptionCheck = function ($exception) use ($that) { - $that->assertInstanceOf('Symfony\Component\Debug\Exception\ContextErrorException', $exception); + $that->assertInstanceOf('Symfony\Component\Debug\Exception\HandledErrorException', $exception); $that->assertEquals(E_NOTICE, $exception->getSeverity()); $that->assertEquals(__FILE__, $exception->getFile()); $that->assertRegexp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage()); @@ -80,7 +80,7 @@ public function testNotice() try { self::triggerNotice($this); - } catch (DummyException $e) { + } catch (HandledErrorException $e) { // if an exception is thrown, the test passed } catch (\Exception $e) { restore_error_handler(); @@ -213,7 +213,7 @@ public function testFatalErrorHandlers($error, $class, $translatedMessage) $m = new \ReflectionMethod($handler, 'handleFatalError'); $m->setAccessible(true); - $m->invoke($handler, $exceptionHandler, $error); + $m->invoke($handler, array($exceptionHandler, 'handle'), $error); $this->assertInstanceof($class, $exceptionHandler->e); // class names are case insensitive and PHP/HHVM do not return the same diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index bf2dbf014c776..48cde6695b2d6 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -11,8 +11,8 @@ namespace Symfony\Component\HttpKernel\DataCollector; +use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Debug\ErrorHandler; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php index 13940ab7ac7b6..ec6b7f9328877 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpKernel\Debug\ErrorHandler; +use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\KernelEvents; diff --git a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php b/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php new file mode 100644 index 0000000000000..39ccfaa0cc855 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Injects a fatal error exceptions handler into the ErrorHandler. + * + * @author Nicolas Grekas + */ +class FatalErrorExceptionsListener implements EventSubscriberInterface +{ + private $handler = null; + + public function __construct($handler) + { + if (is_callable($handler)) { + $this->handler = $handler; + } + } + + public function injectHandler() + { + if ($this->handler) { + ErrorHandler::setFatalErrorExceptionHandler($this->handler); + } + } + + public static function getSubscribedEvents() + { + return array(KernelEvents::REQUEST => 'injectHandler'); + } +} diff --git a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php index c6ca3d475e882..8de07184e38c7 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Debug\Exception\HandledErrorException; /** * Implements the inline rendering strategy where the Request is rendered by the current HTTP kernel. @@ -86,10 +87,15 @@ public function render($uri, Request $request, array $options = array()) } catch (\Exception $e) { // we dispatch the exception event to trigger the logging // the response that comes back is simply ignored - if (isset($options['ignore_errors']) && $options['ignore_errors'] && $this->dispatcher) { - $event = new GetResponseForExceptionEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $e); + if (isset($options['ignore_errors']) && $options['ignore_errors']) { + if ($e instanceof HandledErrorException) { + $e->cleanOutput(); + } + if ($this->dispatcher) { + $event = new GetResponseForExceptionEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $e); - $this->dispatcher->dispatch(KernelEvents::EXCEPTION, $event); + $this->dispatcher->dispatch(KernelEvents::EXCEPTION, $event); + } } // let's clean up the output buffers that were created by the sub-request diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index fda4075a7ea81..46e8a1fe12d5f 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -25,6 +25,8 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Debug\Exception\HandledErrorException; +use Symfony\Component\Debug\Exception\FatalErrorException; /** * HttpKernel notifies events to convert a Request object to a Response one. @@ -70,6 +72,9 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ throw $e; } + if ($e instanceof HandledErrorException) { + $e->cleanOutput(); + } return $this->handleException($e, $request, $type); } @@ -85,6 +90,21 @@ public function terminate(Request $request, Response $response) $this->dispatcher->dispatch(KernelEvents::TERMINATE, new PostResponseEvent($this, $request, $response)); } + /** + * @internal + */ + public function handleFatalErrorException(FatalErrorException $exception) + { + $request = $this->requestStack->getMasterRequest(); + $response = $this->handleException($exception, $request, self::MASTER_REQUEST); + + $response->sendHeaders(); + $response->sendContent(); + + $this->terminate($request, $response); + $exception->cleanOutput(); + } + /** * Handles a request to convert it to a response. * diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 3854dd389d1de..dd01364985504 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -19,7 +19,7 @@ "php": ">=5.3.3", "symfony/event-dispatcher": "~2.1", "symfony/http-foundation": "~2.4", - "symfony/debug": "~2.3", + "symfony/debug": "~2.5", "psr/log": "~1.0" }, "require-dev": { From aa5d62d8bc7f85948190bfd7125be5a40c42b5fe Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 23 Apr 2014 17:17:21 +0200 Subject: [PATCH 130/323] fixed typo --- src/Symfony/Component/Validator/Constraints/Callback.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/Callback.php b/src/Symfony/Component/Validator/Constraints/Callback.php index 5d35762a1c8c8..312952a7fa2f8 100644 --- a/src/Symfony/Component/Validator/Constraints/Callback.php +++ b/src/Symfony/Component/Validator/Constraints/Callback.php @@ -15,7 +15,7 @@ /** * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"}) * * @author Bernhard Schussek * From e3566c27b7093647b4635a5c0ab6c6a67b78287d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 24 Apr 2014 14:55:37 +0200 Subject: [PATCH 131/323] [Debug] fix #10771 DebugClassLoader can't load PSR4 libs --- .../Component/Debug/DebugClassLoader.php | 47 ++++++++++--------- .../Debug/Tests/DebugClassLoaderTest.php | 18 ++++--- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index b7a10d60e5134..2d768d37f89ed 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -176,32 +176,37 @@ public function loadClass($class) $class = substr($class, 1); } - $i = 0; - $tail = DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; - $len = strlen($tail); - - do { - $tail = substr($tail, $i); - $len -= $i; - - if (0 === substr_compare($file, $tail, -$len, $len, true)) { - if (0 !== substr_compare($file, $tail, -$len, $len, false)) { - if (method_exists($this->classLoader[0], 'getClassMap')) { - $map = $this->classLoader[0]->getClassMap(); - } else { - $map = array(); - } - - if (! isset($map[$class])) { - throw new \RuntimeException(sprintf('Case mismatch between class and source file names: %s vs %s', $class, $file)); + if (preg_match('#([/\\\\][a-zA-Z_\x7F-\xFF][a-zA-Z0-9_\x7F-\xFF]*)+\.(php|hh)$#', $file, $tail)) { + $tail = $tail[0]; + $real = realpath($file); + + if (false !== stripos(PHP_OS, 'darwin')) { + // realpath() on MacOSX doesn't normalize the case of characters, + // let's do it ourselves. This is tricky. + $cwd = getcwd(); + $basename = strrpos($real, '/'); + chdir(substr($real, 0, $basename)); + $basename = substr($real, $basename + 1); + $real = getcwd().'/'; + $h = opendir('.'); + while (false !== $f = readdir($h)) { + if (0 === strcasecmp($f, $basename)) { + $real .= $f; + break; } } + closedir($h); + chdir($cwd); + } - break; + if ( 0 === substr_compare($real, $tail, -strlen($tail), strlen($tail), true) + && 0 !== substr_compare($real, $tail, -strlen($tail), strlen($tail), false) + ) { + throw new \RuntimeException(sprintf('Case mismatch between class and source file names: %s vs %s', $class, $real)); } - } while (false !== $i = strpos($tail, DIRECTORY_SEPARATOR, 1)); + } - if (! $exists) { + if (!$exists) { if (false !== strpos($class, '/')) { throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); } diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index fd98a5116e1fd..d50c7adc7559a 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -27,7 +27,7 @@ protected function setUp() { $this->errorReporting = error_reporting(E_ALL | E_STRICT); $this->loader = new ClassLoader(); - spl_autoload_register(array($this->loader, 'loadClass')); + spl_autoload_register(array($this->loader, 'loadClass'), true, true); DebugClassLoader::enable(); } @@ -68,7 +68,7 @@ public function testUnsilencing() // See below: this will fail with parse error // but this should not be @-silenced. - @ class_exists(__NAMESPACE__.'\TestingUnsilencing', true); + @class_exists(__NAMESPACE__.'\TestingUnsilencing', true); ini_set('log_errors', $bak[0]); ini_set('display_errors', $bak[1]); @@ -138,6 +138,10 @@ class_exists(__NAMESPACE__.'\TestingCaseMismatch', true); */ public function testFileCaseMismatch() { + if (!file_exists(__DIR__.'/Fixtures/CaseMismatch.php')) { + $this->markTestSkipped('Can only be run on case insensitive filesystems'); + } + class_exists(__NAMESPACE__.'\Fixtures\CaseMismatch', true); } @@ -168,7 +172,7 @@ public function loadClass($class) public function getClassMap() { - return array(__NAMESPACE__.'\Fixtures\NotPSR0bis' => __DIR__ . '/Fixtures/notPsr0Bis.php'); + return array(__NAMESPACE__.'\Fixtures\NotPSR0bis' => __DIR__.'/Fixtures/notPsr0Bis.php'); } public function findFile($class) @@ -180,13 +184,13 @@ public function findFile($class) } elseif (__NAMESPACE__.'\TestingCaseMismatch' === $class) { eval('namespace '.__NAMESPACE__.'; class TestingCaseMisMatch {}'); } elseif (__NAMESPACE__.'\Fixtures\CaseMismatch' === $class) { - return __DIR__ . '/Fixtures/casemismatch.php'; + return __DIR__.'/Fixtures/CaseMismatch.php'; } elseif (__NAMESPACE__.'\Fixtures\Psr4CaseMismatch' === $class) { - return __DIR__ . '/Fixtures/psr4/Psr4CaseMismatch.php'; + return __DIR__.'/Fixtures/psr4/Psr4CaseMismatch.php'; } elseif (__NAMESPACE__.'\Fixtures\NotPSR0' === $class) { - return __DIR__ . '/Fixtures/reallyNotPsr0.php'; + return __DIR__.'/Fixtures/reallyNotPsr0.php'; } elseif (__NAMESPACE__.'\Fixtures\NotPSR0bis' === $class) { - return __DIR__ . '/Fixtures/notPsr0Bis.php'; + return __DIR__.'/Fixtures/notPsr0Bis.php'; } } } From 7f7e2d8954aafb2d21ff45c4bc3e4b6bcca630a5 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 25 Apr 2014 13:40:15 +0200 Subject: [PATCH 132/323] [FrameworkBundle] removed support for HHVM built-in web server as it is deprecated now --- .../Command/ServerRunCommand.php | 64 +------------------ 1 file changed, 2 insertions(+), 62 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php index 5e7c924581cfd..cbda402fcc0c3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php @@ -29,7 +29,7 @@ class ServerRunCommand extends ContainerAwareCommand */ public function isEnabled() { - if (version_compare(phpversion(), '5.4.0', '<')) { + if (version_compare(phpversion(), '5.4.0', '<') || defined('HHVM_VERSION')) { return false; } @@ -96,12 +96,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ->locateResource(sprintf('@FrameworkBundle/Resources/config/router_%s.php', $env)) ; - if (defined('HHVM_VERSION')) { - $builder = $this->createHhvmProcessBuilder($input, $output, $env); - } else { - $builder = $this->createPhpProcessBuilder($input, $output, $env); - } - + $builder = $this->createPhpProcessBuilder($input, $output, $env); $builder->setWorkingDirectory($input->getOption('docroot')); $builder->setTimeout(null); $builder->getProcess()->run(function ($type, $buffer) use ($output) { @@ -121,59 +116,4 @@ private function createPhpProcessBuilder(InputInterface $input, OutputInterface return new ProcessBuilder(array(PHP_BINARY, '-S', $input->getArgument('address'), $router)); } - - private function createHhvmProcessBuilder(InputInterface $input, OutputInterface $output, $env) - { - list($ip, $port) = explode(':', $input->getArgument('address')); - - $docroot = realpath($input->getOption('docroot')); - $bootstrap = 'prod' === $env ? 'app.php' : 'app_dev.php'; - $config = <<getContainer()->get('kernel')->getCacheDir().'/hhvm-server-'.md5($config).'.hdf'; - file_put_contents($configFile, $config); - - return new ProcessBuilder(array(PHP_BINARY, '--mode', 'server', '--config', $configFile)); - } } From a702124efb03a85685f9808bf3c8e4d4b65ef9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 10 Jan 2014 01:58:15 +0100 Subject: [PATCH 133/323] [SecurityBundle] added acl:set command --- composer.json | 1 + .../SecurityBundle/Command/SetAclCommand.php | 171 ++++++++++++++++++ .../Functional/Bundle/AclBundle/AclBundle.php | 21 +++ .../Bundle/AclBundle/Entity/Car.php | 22 +++ .../Tests/Functional/SetAclCommandTest.php | 168 +++++++++++++++++ .../Tests/Functional/app/Acl/bundles.php | 8 + .../Tests/Functional/app/Acl/config.yml | 24 +++ .../Bundle/SecurityBundle/composer.json | 3 +- 8 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/AclBundle.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/SetAclCommandTest.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/config.yml diff --git a/composer.json b/composer.json index 3b67c4b3e8ec5..1860fa7b2220a 100644 --- a/composer.json +++ b/composer.json @@ -70,6 +70,7 @@ "doctrine/data-fixtures": "1.0.*", "doctrine/dbal": "~2.2", "doctrine/orm": "~2.2,>=2.2.3", + "doctrine/doctrine-bundle": "~1.2", "monolog/monolog": "~1.3", "propel/propel1": "1.6.*", "ircmaxell/password-compat": "1.0.*", diff --git a/src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php new file mode 100644 index 0000000000000..de01dc6b33d4e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php @@ -0,0 +1,171 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Security\Acl\Domain\ObjectIdentity; +use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; +use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; +use Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException; +use Symfony\Component\Security\Acl\Permission\MaskBuilder; +use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface; + +/** + * Sets ACL for objects + * + * @author Kévin Dunglas + */ +class SetAclCommand extends ContainerAwareCommand +{ + /** + * {@inheritdoc} + */ + public function isEnabled() + { + if (!$this->getContainer()->has('security.acl.provider')) { + return false; + } + + $provider = $this->getContainer()->get('security.acl.provider'); + if (!$provider instanceof MutableAclProviderInterface) { + return false; + } + + return parent::isEnabled(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('acl:set') + ->setDescription('Sets ACL for objects') + ->setHelp(<<%command.name% command sets ACL. +The ACL system must have been initialized with the init:acl command. + +To set VIEW and EDIT permissions for the user kevin on the instance of Acme\MyClass having the identifier 42: + +php %command.full_name% --user=Symfony/Component/Security/Core/User/User:kevin VIEW EDIT Acme/MyClass:42 + +Note that you can use / instead of \\ for the namespace delimiter to avoid any +problem. + +To set permissions for a role, use the --role option: + +php %command.full_name% --role=ROLE_USER VIEW Acme/MyClass:1936 + +To set permissions at the class scope, use the --class-scope option: + +php %command.full_name% --class-scope --user=Symfony/Component/Security/Core/User/User:anne OWNER Acme/MyClass:42 +EOF + ) + ->addArgument('arguments', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'A list of permissions and object identities (class name and ID separated by a column)') + ->addOption('user', null, InputOption::VALUE_REQUIRED, 'A list of security identities') + ->addOption('role', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A list of roles') + ->addOption('class-scope', null, InputOption::VALUE_NONE, 'Use class-scope entries') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + // Parse arguments + $objectIdentities = array(); + $maskBuilder = $this->getMaskBuilder(); + foreach ($input->getArgument('arguments') as $argument) { + $data = explode(':', $argument, 2); + + if (count($data) > 1) { + $objectIdentities[] = new ObjectIdentity($data[1], strtr($data[0], '/', '\\')); + } else { + $maskBuilder->add($data[0]); + } + } + + // Build permissions mask + $mask = $maskBuilder->get(); + + $userOption = $input->getOption('user'); + $roleOption = $input->getOption('role'); + $classScopeOption = $input->getOption('class-scope'); + + if (empty($userOption) && empty($roleOption)) { + throw new \InvalidArgumentException('A Role or a User must be specified.'); + } + + // Create security identities + $securityIdentities = array(); + + if ($userOption) { + foreach ($userOption as $user) { + $data = explode(':', $user, 2); + + if (count($data) === 1) { + throw new \InvalidArgumentException('The user must follow the format "Acme/MyUser:username".'); + } + + $securityIdentities[] = new UserSecurityIdentity($data[1], strtr($data[0], '/', '\\')); + } + } + + if ($roleOption) { + foreach ($roleOption as $role) { + $securityIdentities[] = new RoleSecurityIdentity($role); + } + } + + /** @var $container \Symfony\Component\DependencyInjection\ContainerInterface */ + $container = $this->getContainer(); + /** @var $aclProvider MutableAclProviderInterface */ + $aclProvider = $container->get('security.acl.provider'); + + // Sets ACL + foreach ($objectIdentities as $objectIdentity) { + // Creates a new ACL if it does not already exist + try { + $aclProvider->createAcl($objectIdentity); + } catch (AclAlreadyExistsException $e) { + } + + $acl = $aclProvider->findAcl($objectIdentity, $securityIdentities); + + foreach ($securityIdentities as $securityIdentity) { + if ($classScopeOption) { + $acl->insertClassAce($securityIdentity, $mask); + } else { + $acl->insertObjectAce($securityIdentity, $mask); + } + } + + $aclProvider->updateAcl($acl); + } + } + + /** + * Gets the mask builder + * + * @return MaskBuilder + */ + protected function getMaskBuilder() + { + return new MaskBuilder(); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/AclBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/AclBundle.php new file mode 100644 index 0000000000000..1208003bcc2c4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/AclBundle.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\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Kévin Dunglas + */ +class AclBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.php new file mode 100644 index 0000000000000..dd46db2f6459c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity; + +/** + * Car + * + * @author Kévin Dunglas + */ +class Car +{ + public $id; +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SetAclCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SetAclCommandTest.php new file mode 100644 index 0000000000000..c476714e88f27 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SetAclCommandTest.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\SecurityBundle\Command\InitAclCommand; +use Symfony\Bundle\SecurityBundle\Command\SetAclCommand; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Security\Acl\Domain\ObjectIdentity; +use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; +use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; +use Symfony\Component\Security\Acl\Exception\NoAceFoundException; +use Symfony\Component\Security\Acl\Permission\BasicPermissionMap; + +/** + * Tests SetAclCommand + * + * @author Kévin Dunglas + */ +class SetAclCommandTest extends WebTestCase +{ + const OBJECT_CLASS = 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity\Car'; + const SECURITY_CLASS = 'Symfony\Component\Security\Core\User\User'; + + public function testSetAclUser() + { + $objectId = 1; + $securityUsername1 = 'kevin'; + $securityUsername2 = 'anne'; + $grantedPermission1 = 'VIEW'; + $grantedPermission2 = 'EDIT'; + + $application = $this->getApplication(); + $application->add(new SetAclCommand()); + + $setAclCommand = $application->find('acl:set'); + $setAclCommandTester = new CommandTester($setAclCommand); + $setAclCommandTester->execute(array( + 'command' => 'acl:set', + 'arguments' => array($grantedPermission1, $grantedPermission2, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)), + '--user' => array(sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername1), sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername2)) + )); + + $objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS); + $securityIdentity1 = new UserSecurityIdentity($securityUsername1, self::SECURITY_CLASS); + $securityIdentity2 = new UserSecurityIdentity($securityUsername2, self::SECURITY_CLASS); + $permissionMap = new BasicPermissionMap(); + + /** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */ + $aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider'); + $acl = $aclProvider->findAcl($objectIdentity, array($securityIdentity1)); + + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity1))); + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity2))); + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission2, null), array($securityIdentity2))); + + try { + $acl->isGranted($permissionMap->getMasks('OWNER', null), array($securityIdentity1)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + + try { + $acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($securityIdentity2)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + } + + public function testSetAclRole() + { + $objectId = 1; + $securityUsername = 'kevin'; + $grantedPermission = 'VIEW'; + $role = 'ROLE_ADMIN'; + + $application = $this->getApplication(); + $application->add(new SetAclCommand()); + + $setAclCommand = $application->find('acl:set'); + $setAclCommandTester = new CommandTester($setAclCommand); + $setAclCommandTester->execute(array( + 'command' => 'acl:set', + 'arguments' => array($grantedPermission, sprintf('%s:%s', strtr(self::OBJECT_CLASS, '\\', '/'), $objectId)), + '--role' => array($role) + )); + + $objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS); + $userSecurityIdentity = new UserSecurityIdentity($securityUsername, self::SECURITY_CLASS); + $roleSecurityIdentity = new RoleSecurityIdentity($role); + $permissionMap = new BasicPermissionMap(); + + /** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */ + $aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider'); + $acl = $aclProvider->findAcl($objectIdentity, array($roleSecurityIdentity, $userSecurityIdentity)); + + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + + try { + $acl->isGranted($permissionMap->getMasks('VIEW', null), array($userSecurityIdentity)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + + try { + $acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($userSecurityIdentity)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + } + + public function testSetAclClassScope() + { + $objectId = 1; + $grantedPermission = 'VIEW'; + $role = 'ROLE_USER'; + + $application = $this->getApplication(); + $application->add(new SetAclCommand()); + + $setAclCommand = $application->find('acl:set'); + $setAclCommandTester = new CommandTester($setAclCommand); + $setAclCommandTester->execute(array( + 'command' => 'acl:set', + 'arguments' => array($grantedPermission, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)), + '--class-scope' => true, + '--role' => array($role) + )); + + $objectIdentity1 = new ObjectIdentity($objectId, self::OBJECT_CLASS); + $objectIdentity2 = new ObjectIdentity(2, self::OBJECT_CLASS); + $roleSecurityIdentity = new RoleSecurityIdentity($role); + $permissionMap = new BasicPermissionMap(); + + /** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */ + $aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider'); + + $acl1 = $aclProvider->findAcl($objectIdentity1, array($roleSecurityIdentity)); + $this->assertTrue($acl1->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + + $acl2 = $aclProvider->createAcl($objectIdentity2); + $this->assertTrue($acl2->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + } + + private function getApplication() + { + $kernel = $this->createKernel(array('test_case' => 'Acl')); + $kernel->boot(); + + $application = new Application($kernel); + $application->add(new InitAclCommand()); + + $initAclCommand = $application->find('init:acl'); + $initAclCommandTester = new CommandTester($initAclCommand); + $initAclCommandTester->execute(array('command' => 'init:acl')); + + return $application; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php new file mode 100644 index 0000000000000..0dad9099b493d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php @@ -0,0 +1,8 @@ + Date: Sun, 27 Apr 2014 11:03:19 +0200 Subject: [PATCH 134/323] Allow overloading ContextListener::refreshUser() --- .../Component/Security/Http/Firewall/ContextListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 435c44026ead2..e61907eff302b 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -142,7 +142,7 @@ public function onKernelResponse(FilterResponseEvent $event) * * @throws \RuntimeException */ - private function refreshUser(TokenInterface $token) + protected function refreshUser(TokenInterface $token) { $user = $token->getUser(); if (!$user instanceof UserInterface) { From 3a5a525aeedfff5243d127ea38df5101aa320d54 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Mon, 28 Apr 2014 14:32:01 +0200 Subject: [PATCH 135/323] [Console] Fix #10795: Allow instancing Console Application when STDIN is not declared --- .../Console/Helper/QuestionHelper.php | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index a77c052a44b26..7ac2afa0ac304 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -28,11 +28,6 @@ class QuestionHelper extends Helper private static $shell; private static $stty; - public function __construct() - { - $this->inputStream = STDIN; - } - /** * Asks a question to the user. * @@ -114,6 +109,8 @@ public function getName() */ public function doAsk(OutputInterface $output, Question $question) { + $inputStream = $this->inputStream ?: STDIN; + $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { $width = max(array_map('strlen', array_keys($question->getChoices()))); @@ -135,7 +132,7 @@ public function doAsk(OutputInterface $output, Question $question) $ret = false; if ($question->isHidden()) { try { - $ret = trim($this->getHiddenResponse($output)); + $ret = trim($this->getHiddenResponse($output, $inputStream)); } catch (\RuntimeException $e) { if (!$question->isHiddenFallback()) { throw $e; @@ -144,14 +141,14 @@ public function doAsk(OutputInterface $output, Question $question) } if (false === $ret) { - $ret = fgets($this->inputStream, 4096); + $ret = fgets($inputStream, 4096); if (false === $ret) { throw new \RuntimeException('Aborted'); } $ret = trim($ret); } } else { - $ret = trim($this->autocomplete($output, $question)); + $ret = trim($this->autocomplete($output, $question, $inputStream)); } $ret = strlen($ret) > 0 ? $ret : $question->getDefault(); @@ -171,7 +168,7 @@ public function doAsk(OutputInterface $output, Question $question) * * @return string */ - private function autocomplete(OutputInterface $output, Question $question) + private function autocomplete(OutputInterface $output, Question $question, $inputStream) { $autocomplete = $question->getAutocompleterValues(); $ret = ''; @@ -190,8 +187,8 @@ private function autocomplete(OutputInterface $output, Question $question) $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); // Read a keypress - while (!feof($this->inputStream)) { - $c = fread($this->inputStream, 1); + while (!feof($inputStream)) { + $c = fread($inputStream, 1); // Backspace Character if ("\177" === $c) { @@ -212,7 +209,7 @@ private function autocomplete(OutputInterface $output, Question $question) // Pop the last character off the end of our string $ret = substr($ret, 0, $i); } elseif ("\033" === $c) { // Did we read an escape sequence? - $c .= fread($this->inputStream, 2); + $c .= fread($inputStream, 2); // A = Up Arrow. B = Down Arrow if ('A' === $c[2] || 'B' === $c[2]) { @@ -289,7 +286,7 @@ private function autocomplete(OutputInterface $output, Question $question) * * @throws \RuntimeException In case the fallback is deactivated and the response cannot be hidden */ - private function getHiddenResponse(OutputInterface $output) + private function getHiddenResponse(OutputInterface $output, $inputStream) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; @@ -315,7 +312,7 @@ private function getHiddenResponse(OutputInterface $output) $sttyMode = shell_exec('stty -g'); shell_exec('stty -echo'); - $value = fgets($this->inputStream, 4096); + $value = fgets($inputStream, 4096); shell_exec(sprintf('stty %s', $sttyMode)); if (false === $value) { From d7a186f5ca088c13d3b10ba5e8d07d61cc73200b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 27 Apr 2014 09:44:26 +0000 Subject: [PATCH 136/323] [Debug] less intrusive work around for https://bugs.php.net/54275 --- src/Symfony/Component/Debug/CHANGELOG.md | 2 - src/Symfony/Component/Debug/ErrorHandler.php | 144 ++++++++++++++---- .../Debug/Exception/ContextErrorException.php | 19 ++- .../Debug/Exception/FatalErrorException.php | 2 +- .../Debug/Exception/HandledErrorException.php | 86 ----------- .../Debug/Tests/DebugClassLoaderTest.php | 37 ++--- .../Debug/Tests/ErrorHandlerTest.php | 55 +++---- .../EventListener/ErrorsLoggerListener.php | 3 +- .../FatalErrorExceptionsListener.php | 4 +- .../Fragment/InlineFragmentRenderer.php | 12 +- .../Component/HttpKernel/HttpKernel.php | 5 - 11 files changed, 173 insertions(+), 196 deletions(-) delete mode 100644 src/Symfony/Component/Debug/Exception/HandledErrorException.php diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index 3823fc8783563..b128efaaa8913 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -4,11 +4,9 @@ CHANGELOG 2.5.0 ----- -* added HandledErrorException * added ErrorHandler::setFatalErrorExceptionHandler() * added UndefinedMethodFatalErrorHandler * deprecated ExceptionHandlerInterface -* deprecated ContextErrorException * deprecated DummyException 2.4.0 diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 46296f45e7fd5..e1bad044ba612 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -13,9 +13,8 @@ use Psr\Log\LogLevel; use Psr\Log\LoggerInterface; +use Symfony\Component\Debug\Exception\ContextErrorException; use Symfony\Component\Debug\Exception\FatalErrorException; -use Symfony\Component\Debug\Exception\DummyException; -use Symfony\Component\Debug\Exception\HandledErrorException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; @@ -54,6 +53,8 @@ class ErrorHandler private $displayErrors; + private $caughtOutput = 0; + /** * @var LoggerInterface[] Loggers for channels */ @@ -129,7 +130,7 @@ public static function setFatalErrorExceptionHandler($handler) } /** - * @throws HandledErrorException When error_reporting returns error + * @throws ContextErrorException When error_reporting returns error */ public function handle($level, $message, $file = 'unknown', $line = 0, $context = array()) { @@ -159,36 +160,24 @@ function ($row) { } if ($this->displayErrors && error_reporting() & $level && $this->level & $level) { - // Exceptions thrown from error handlers are sometimes not caught by the exception - // handler, so we invoke it directly (https://bugs.php.net/bug.php?id=54275) - $exceptionHandler = set_exception_handler('var_dump'); - restore_exception_handler(); + if (self::$stackedErrorLevels) { + self::$stackedErrors[] = func_get_args(); - if ($exceptionHandler) { - if (self::$stackedErrorLevels) { - self::$stackedErrors[] = func_get_args(); + return true; + } - return true; - } + $exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line); + $exception = new ContextErrorException($exception, 0, $level, $file, $line, $context); - $exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line); - $exception = new HandledErrorException($exception, 0, $level, $file, $line, $context); - $exception->handleWith($exceptionHandler); - - // we must stop the PHP script execution, as the exception has - // already been dealt with, so, let's throw an exception that - // will be caught by a dummy exception handler - set_exception_handler(function (\Exception $e) use ($exceptionHandler) { - if (!$e instanceof HandledErrorException && !$e instanceof DummyException) { - // happens if our handled exception is caught by a - // catch-all from user code, in which case, let the - // current handler handle this "new" exception - call_user_func($exceptionHandler, $e); - } - }); + if (PHP_VERSION_ID <= 50407 && (PHP_VERSION_ID >= 50400 || PHP_VERSION_ID <= 50317)) { + // Exceptions thrown from error handlers are sometimes not caught by the exception + // handler and shutdown handlers are bypassed before 5.4.8/5.3.18. + // We temporarily re-enable display_errors to prevent any blank page related to this bug. - throw $exception; + $exception->errorHandlerCanary = new ErrorHandlerCanary(); } + + throw $exception; } if (isset(self::$loggers['scream']) && !(error_reporting() & $level)) { @@ -323,23 +312,114 @@ protected function getFatalErrorHandlers() private function handleFatalError($exceptionHandler, array $error) { + // Let PHP handle any further error + set_error_handler('var_dump', 0); + ini_set('display_errors', 1); + $level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type']; $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line'], 3); foreach ($this->getFatalErrorHandlers() as $handler) { - if ($ex = $handler->handleError($error, $exception)) { - $exception = $ex; + if ($e = $handler->handleError($error, $exception)) { + $exception = $e; break; } } + // To be as fail-safe as possible, the FatalErrorException is first handled + // by the exception handler, then by the fatal error handler. The latter takes + // precedence and any output from the former is cancelled, if and only if + // nothing bad happens in this handling path. + + $caughtOutput = 0; + if ($exceptionHandler) { - $exception->handleWith($exceptionHandler); + $this->caughtOutput = false; + ob_start(array($this, 'catchOutput')); + try { + call_user_func($exceptionHandler, $exception); + } catch (\Exception $e) { + // Ignore this exception, we have to deal with the fatal error + } + if (false === $this->caughtOutput) { + ob_end_clean(); + } + if (isset($this->caughtOutput[0])) { + ob_start(array($this, 'cleanOutput')); + echo $this->caughtOutput; + $caughtOutput = ob_get_length(); + } + $this->caughtOutput = 0; } if (self::$fatalHandler) { - call_user_func(self::$fatalHandler, $exception); + try { + call_user_func(self::$fatalHandler, $exception); + + if ($caughtOutput) { + $this->caughtOutput = $caughtOutput; + } + } catch (\Exception $e) { + if (!$caughtOutput) { + // Neither the exception nor the fatal handler succeeded. + // Let PHP handle that now. + throw $exception; + } + } + } + } + + /** + * @internal + */ + public function catchOutput($buffer) + { + $this->caughtOutput = $buffer; + + return ''; + } + + /** + * @internal + */ + public function cleanOutput($buffer) + { + if ($this->caughtOutput) { + // use substr_replace() instead of substr() for mbstring overloading resistance + $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtOutput); + if (isset($cleanBuffer[0])) { + $buffer = $cleanBuffer; + } + } + + return $buffer; + } +} + +/** + * Private class used to work around https://bugs.php.net/54275 + * + * @author Nicolas Grekas + * + * @internal + */ +class ErrorHandlerCanary +{ + private static $displayErrors = null; + + public function __construct() + { + if (null === self::$displayErrors) { + self::$displayErrors = ini_set('display_errors', 1); + } + } + + public function __destruct() + { + if (null !== self::$displayErrors) { + ini_set('display_errors', self::$displayErrors); + self::$displayErrors = null; } } } diff --git a/src/Symfony/Component/Debug/Exception/ContextErrorException.php b/src/Symfony/Component/Debug/Exception/ContextErrorException.php index d6a9eaf3da2b2..54f0198f1b2f8 100644 --- a/src/Symfony/Component/Debug/Exception/ContextErrorException.php +++ b/src/Symfony/Component/Debug/Exception/ContextErrorException.php @@ -15,9 +15,22 @@ * Error Exception with Variable Context. * * @author Christian Sciberras - * - * @deprecated since version 2.5, to be removed in 3.0. */ -class ContextErrorException extends HandledErrorException +class ContextErrorException extends \ErrorException { + private $context = array(); + + public function __construct($message, $code, $severity, $filename, $lineno, $context = array()) + { + parent::__construct($message, $code, $severity, $filename, $lineno); + $this->context = $context; + } + + /** + * @return array Array of variables that existed when the exception occurred + */ + public function getContext() + { + return $this->context; + } } diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 47375a370079f..4e29495f302cb 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -18,7 +18,7 @@ * @author Konstanton Myakshin * @author Nicolas Grekas */ -class FatalErrorException extends HandledErrorException +class FatalErrorException extends \ErrorException { public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null) { diff --git a/src/Symfony/Component/Debug/Exception/HandledErrorException.php b/src/Symfony/Component/Debug/Exception/HandledErrorException.php deleted file mode 100644 index 83219d119689c..0000000000000 --- a/src/Symfony/Component/Debug/Exception/HandledErrorException.php +++ /dev/null @@ -1,86 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Debug\Exception; - -/** - * @author Nicolas Grekas - */ -class HandledErrorException extends \ErrorException -{ - private $handlerOutput = false; - private $context = array(); - - public function __construct($message, $code, $severity, $filename, $lineno, $context = array()) - { - parent::__construct($message, $code, $severity, $filename, $lineno); - $this->context = $context; - } - - /** - * @return array Array of variables that existed when the exception occurred - */ - public function getContext() - { - return $this->context; - } - - public function handleWith($exceptionHandler) - { - $this->handlerOutput = false; - ob_start(array($this, 'catchOutput')); - call_user_func($exceptionHandler, $this); - if (false === $this->handlerOutput) { - ob_end_clean(); - } - ob_start(array(__CLASS__, 'flushOutput')); - echo $this->handlerOutput; - $this->handlerOutput = ob_get_length(); - } - - /** - * @internal - */ - public function catchOutput($buffer) - { - $this->handlerOutput = $buffer; - - return ''; - } - - /** - * @internal - */ - public static function flushOutput($buffer) - { - return $buffer; - } - - public function cleanOutput() - { - $status = ob_get_status(); - - if (isset($status['name']) && __CLASS__.'::flushOutput' === $status['name']) { - if ($this->handlerOutput) { - // use substr_replace() instead of substr() for mbstring overloading resistance - echo substr_replace(ob_get_clean(), '', 0, $this->handlerOutput); - } else { - ob_end_flush(); - } - } - } - - public function __destruct() - { - $this->handlerOutput = 0; - $this->cleanOutput(); - } -} diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index d50c7adc7559a..a002e22d0bbe2 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Debug\DebugClassLoader; use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\Exception\ContextErrorException; class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase { @@ -77,52 +78,40 @@ public function testUnsilencing() $this->assertStringMatchesFormat('%aParse error%a', $output); } - /** - * @expectedException \Symfony\Component\Debug\Exception\HandledErrorException - */ public function testStacking() { - // the HandledErrorException must not be loaded to test the workaround + // the ContextErrorException must not be loaded to test the workaround // for https://bugs.php.net/65322. - if (class_exists('Symfony\Component\Debug\Exception\HandledErrorException', false)) { - $this->markTestSkipped('The HandledErrorException class is already loaded.'); + if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) { + $this->markTestSkipped('The ContextErrorException class is already loaded.'); } - $exceptionHandler = $this->getMock('Symfony\Component\Debug\ExceptionHandler', array('handle')); - set_exception_handler(array($exceptionHandler, 'handle')); - - $that = $this; - $exceptionCheck = function ($exception) use ($that) { - $that->assertInstanceOf('Symfony\Component\Debug\Exception\HandledErrorException', $exception); - $that->assertEquals(E_STRICT, $exception->getSeverity()); - $that->assertStringStartsWith(__FILE__, $exception->getFile()); - $that->assertRegexp('/^Runtime Notice: Declaration/', $exception->getMessage()); - }; - - $exceptionHandler->expects($this->once()) - ->method('handle') - ->will($this->returnCallback($exceptionCheck)); ErrorHandler::register(); try { // Trigger autoloading + E_STRICT at compile time // which in turn triggers $errorHandler->handle() - // that again triggers autoloading for HandledErrorException. + // that again triggers autoloading for ContextErrorException. // Error stacking works around the bug above and everything is fine. eval(' namespace '.__NAMESPACE__.'; class ChildTestingStacking extends TestingStacking { function foo($bar) {} } '); + $this->fail('ContextErrorException expected'); + } catch (ContextErrorException $exception) { + // if an exception is thrown, the test passed + restore_error_handler(); + restore_exception_handler(); + $this->assertEquals(E_STRICT, $exception->getSeverity()); + $this->assertStringStartsWith(__FILE__, $exception->getFile()); + $this->assertRegexp('/^Runtime Notice: Declaration/', $exception->getMessage()); } catch (\Exception $e) { restore_error_handler(); restore_exception_handler(); throw $e; } - - restore_error_handler(); - restore_exception_handler(); } /** diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 7786c14ef13a6..1bb16712e8c42 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Debug\Tests; use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\Exception\HandledErrorException; +use Symfony\Component\Debug\Exception\ContextErrorException; /** * ErrorHandlerTest @@ -46,42 +46,33 @@ public function tearDown() public function testNotice() { - $exceptionHandler = $this->getMock('Symfony\Component\Debug\ExceptionHandler', array('handle')); - set_exception_handler(array($exceptionHandler, 'handle')); - - $that = $this; - $exceptionCheck = function ($exception) use ($that) { - $that->assertInstanceOf('Symfony\Component\Debug\Exception\HandledErrorException', $exception); - $that->assertEquals(E_NOTICE, $exception->getSeverity()); - $that->assertEquals(__FILE__, $exception->getFile()); - $that->assertRegexp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage()); - $that->assertArrayHasKey('foobar', $exception->getContext()); - - $trace = $exception->getTrace(); - $that->assertEquals(__FILE__, $trace[0]['file']); - $that->assertEquals('Symfony\Component\Debug\ErrorHandler', $trace[0]['class']); - $that->assertEquals('handle', $trace[0]['function']); - $that->assertEquals('->', $trace[0]['type']); - - $that->assertEquals(__FILE__, $trace[1]['file']); - $that->assertEquals(__CLASS__, $trace[1]['class']); - $that->assertEquals('triggerNotice', $trace[1]['function']); - $that->assertEquals('::', $trace[1]['type']); - - $that->assertEquals(__CLASS__, $trace[2]['class']); - $that->assertEquals('testNotice', $trace[2]['function']); - $that->assertEquals('->', $trace[2]['type']); - }; - - $exceptionHandler->expects($this->once()) - ->method('handle') - ->will($this->returnCallback($exceptionCheck)); ErrorHandler::register(); try { self::triggerNotice($this); - } catch (HandledErrorException $e) { + $this->fail('ContextErrorException expected'); + } catch (ContextErrorException $exception) { // if an exception is thrown, the test passed + restore_error_handler(); + $this->assertEquals(E_NOTICE, $exception->getSeverity()); + $this->assertEquals(__FILE__, $exception->getFile()); + $this->assertRegexp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage()); + $this->assertArrayHasKey('foobar', $exception->getContext()); + + $trace = $exception->getTrace(); + $this->assertEquals(__FILE__, $trace[0]['file']); + $this->assertEquals('Symfony\Component\Debug\ErrorHandler', $trace[0]['class']); + $this->assertEquals('handle', $trace[0]['function']); + $this->assertEquals('->', $trace[0]['type']); + + $this->assertEquals(__FILE__, $trace[1]['file']); + $this->assertEquals(__CLASS__, $trace[1]['class']); + $this->assertEquals('triggerNotice', $trace[1]['function']); + $this->assertEquals('::', $trace[1]['type']); + + $this->assertEquals(__CLASS__, $trace[2]['class']); + $this->assertEquals('testNotice', $trace[2]['function']); + $this->assertEquals('->', $trace[2]['type']); } catch (\Exception $e) { restore_error_handler(); diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php index ec6b7f9328877..025b6475a4632 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorsLoggerListener.php @@ -38,11 +38,12 @@ public function injectLogger() { if (null !== $this->logger) { ErrorHandler::setLogger($this->logger, $this->channel); + $this->logger = null; } } public static function getSubscribedEvents() { - return array(KernelEvents::REQUEST => 'injectLogger'); + return array(KernelEvents::REQUEST => array('injectLogger', 2048)); } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php b/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php index 39ccfaa0cc855..0677682810ea8 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php @@ -35,11 +35,13 @@ public function injectHandler() { if ($this->handler) { ErrorHandler::setFatalErrorExceptionHandler($this->handler); + $this->handler = null; } } public static function getSubscribedEvents() { - return array(KernelEvents::REQUEST => 'injectHandler'); + // Don't register early as e.g. the Router is generally required by the handler + return array(KernelEvents::REQUEST => array('injectHandler', 8)); } } diff --git a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php index 33866e94ea9c5..a6ab82ea28efa 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php @@ -18,7 +18,6 @@ use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Debug\Exception\HandledErrorException; /** * Implements the inline rendering strategy where the Request is rendered by the current HTTP kernel. @@ -87,15 +86,10 @@ public function render($uri, Request $request, array $options = array()) } catch (\Exception $e) { // we dispatch the exception event to trigger the logging // the response that comes back is simply ignored - if (isset($options['ignore_errors']) && $options['ignore_errors']) { - if ($e instanceof HandledErrorException) { - $e->cleanOutput(); - } - if ($this->dispatcher) { - $event = new GetResponseForExceptionEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $e); + if (isset($options['ignore_errors']) && $options['ignore_errors'] && $this->dispatcher) { + $event = new GetResponseForExceptionEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $e); - $this->dispatcher->dispatch(KernelEvents::EXCEPTION, $event); - } + $this->dispatcher->dispatch(KernelEvents::EXCEPTION, $event); } // let's clean up the output buffers that were created by the sub-request diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 46e8a1fe12d5f..c3556972e97d1 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -25,7 +25,6 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Debug\Exception\HandledErrorException; use Symfony\Component\Debug\Exception\FatalErrorException; /** @@ -72,9 +71,6 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ throw $e; } - if ($e instanceof HandledErrorException) { - $e->cleanOutput(); - } return $this->handleException($e, $request, $type); } @@ -102,7 +98,6 @@ public function handleFatalErrorException(FatalErrorException $exception) $response->sendContent(); $this->terminate($request, $response); - $exception->cleanOutput(); } /** From 0126c56dd367e060e3443223b6aae5de87b9711d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 29 Apr 2014 09:08:09 +0200 Subject: [PATCH 137/323] updated CHANGELOG for 2.5.0-BETA2 --- CHANGELOG-2.5.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG-2.5.md b/CHANGELOG-2.5.md index 3dc59e0b13ad3..dabbff17a044f 100644 --- a/CHANGELOG-2.5.md +++ b/CHANGELOG-2.5.md @@ -7,6 +7,35 @@ in 2.5 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.5.0...v2.5.1 +* 2.5.0-BETA2 (2014-04-29) + + * bug #10803 [Debug] fix ErrorHandlerTest when context is not an array (nicolas-grekas) + * bug #10801 [Debug] ErrorHandler: remove $GLOBALS from context in PHP5.3 fix #10292 (nicolas-grekas) + * bug #10799 [Debug] less intrusive work around for https://bugs.php.net/54275 (nicolas-grekas) + * bug #10797 [HttpFoundation] Allow File instance to be passed to BinaryFileResponse (anlutro) + * bug #10798 [Console] Fix #10795: Allow instancing Console Application when STDIN is not declared (romainneutron) + * bug #10643 [TwigBridge] Removed strict check when found variables inside a translation (goetas) + * bug #10605 [ExpressionLanguage] Strict in_array check in Parser.php (parnas) + * bug #10789 [Console] Fixed the rendering of exceptions on HHVM with a terminal width (stof) + * bug #10773 [WebProfilerBundle ] Fixed an edge case on WDT loading (tucksaun) + * feature #10786 [FrameworkBundle] removed support for HHVM built-in web server as it is deprecated now (fabpot) + * bug #10784 [Security] removed $csrfTokenManager type hint from SimpleFormAuthenticationListener constructor argument (choonge) + * bug #10776 [Debug] fix #10771 DebugClassLoader can't load PSR4 libs (nicolas-grekas) + * bug #10763 [Process] Disable TTY mode on Windows platform (romainneutron) + * bug #10772 [Finder] Fix ignoring of unreadable dirs in the RecursiveDirectoryIterator (jakzal) + * bug #10757 [Process] Setting STDIN while running should not be possible (romainneutron) + * bug #10749 Fixed incompatibility of x509 auth with nginx (alcaeus) + * feature #10725 [Debug] Handled errors (nicolas-grekas) + * bug #10735 [Translation] [PluralizationRules] Little correction for case 'ar' (klyk50) + * bug #10720 [HttpFoundation] Fix DbalSessionHandler (Tobion) + * bug #10721 [HttpFoundation] status 201 is allowed to have a body (Tobion) + * bug #10728 [Process] Fix #10681, process are failing on Windows Server 2003 (romainneutron) + * bug #10733 [DomCrawler] Textarea value should default to empty string instead of null. (Berdir) + * bug #10723 [Security] fix DBAL connection typehint (Tobion) + * bug #10715 [Debug] Fixed ClassNotFoundFatalErrorHandler on windows. (lyrixx) + * bug #10700 Fixes various inconsistencies in the code (fabpot) + * bug #10697 [Translation] Make IcuDatFileLoader/IcuResFileLoader::load invalid resource compatible with HHVM. (idn2104) + * 2.5.0-BETA1 (2014-04-11) * first beta release From aea2c6427e5df38526af7b3d5ef46fba3a2fde0c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 29 Apr 2014 09:08:19 +0200 Subject: [PATCH 138/323] updated VERSION for 2.5.0-BETA2 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 7685d08dfc92a..86a610f5d3898 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-DEV'; + const VERSION = '2.5.0-BETA2'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = 'BETA2'; /** * Constructor. From 939e92d3ca261bd5511743e963c76b1572ac11e9 Mon Sep 17 00:00:00 2001 From: Julien Pauli Date: Tue, 29 Apr 2014 12:13:57 +0200 Subject: [PATCH 139/323] Fixed composer to include config component for mocks in phpunit --- src/Symfony/Component/EventDispatcher/composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 6343b5d1d9c74..3715ece302fb1 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "symfony/dependency-injection": "~2.0", + "symfony/config": "~2.0", "symfony/stopwatch": "~2.2", "psr/log": "~1.0" }, From 89f7907c4fc6e29be6a239fc6c618e3b32ad341b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 29 Apr 2014 12:38:03 +0200 Subject: [PATCH 140/323] bumped Symfony version to 2.5.0 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 86a610f5d3898..7685d08dfc92a 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-BETA2'; + const VERSION = '2.5.0-DEV'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'BETA2'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From e40153354da1e014d86e5a688a4059f2c397fdb2 Mon Sep 17 00:00:00 2001 From: Ruben Kruiswijk Date: Tue, 29 Apr 2014 13:32:21 +0200 Subject: [PATCH 141/323] Use absolute URLs to assets on Symfony its internal pages. --- .../Bundle/TwigBundle/Resources/views/layout.html.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig index 49f997b5041d0..69409827057d0 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig @@ -4,8 +4,8 @@ {% block title %}{% endblock %} - - + + {% block head %}{% endblock %} From afca3cdf2dfaf84858bca90d316edc2f2bea0202 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 30 Apr 2014 08:24:35 +0200 Subject: [PATCH 142/323] Revert "bug #10817 [Debug] fix #10313: FlattenException not found (nicolas-grekas)" reverted in 2.5 as it does not make sense here. This reverts commit c18bf19016cc96dcf1604c0a5c97b647c78cb63d, reversing changes made to 585b61db95dd63f8d048648384f5243320a44847. --- src/Symfony/Component/Debug/ErrorHandler.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 1240ee8df63d4..c6b71c36ab712 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -165,9 +165,6 @@ function ($row) { return true; } - if (!class_exists('Symfony\Component\Debug\Exception\FlattenException')) { - require __DIR__.'/Exception/FlattenException.php'; - } if (PHP_VERSION_ID < 50400 && isset($context['GLOBALS']) && is_array($context)) { unset($context['GLOBALS']); From 418cea174df1b7dd69926431c34f254a5c41076b Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 2 May 2014 17:24:43 +0000 Subject: [PATCH 143/323] [TwigBridge] Added compile-time issues checking in twig:lint command --- src/Symfony/Bridge/Twig/Command/LintCommand.php | 12 +++++++++--- .../Bridge/Twig/Tests/Command/LintCommandTest.php | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index 79cbfce22b5f2..08525d2daaeaa 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -99,7 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $template .= fread(STDIN, 1024); } - return $this->display($input, $output, array($this->validate($twig, $template))); + return $this->display($input, $output, array($this->validate($twig, $template, uniqid('sf_')))); } $filesInfo = array(); @@ -121,11 +121,17 @@ protected function findFiles($filename) throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); } - private function validate(\Twig_Environment $twig, $template, $file = null) + private function validate(\Twig_Environment $twig, $template, $file) { + $realLoader = $twig->getLoader(); try { - $twig->parse($twig->tokenize($template, $file ? (string) $file : null)); + $temporaryLoader = new \Twig_Loader_Array(array((string) $file => $template)); + $twig->setLoader($temporaryLoader); + $nodeTree = $twig->parse($twig->tokenize($template, (string) $file)); + $twig->compile($nodeTree); + $twig->setLoader($realLoader); } catch (\Twig_Error $e) { + $twig->setLoader($realLoader); return array('template' => $template, 'file' => $file, 'valid' => false, 'exception' => $e); } diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 3fe54cbabf547..8d5bd991368e2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -57,6 +57,17 @@ public function testLintFileNotReadable() $ret = $tester->execute(array('filename' => $filename)); } + public function testLintFileCompileTimeException() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile("{{ 2|number_format(2, decimal_point='.', ',') }}"); + + $ret = $tester->execute(array('filename' => $filename)); + + $this->assertEquals(1, $ret, 'Returns 1 in case of error'); + $this->assertRegExp('/^KO in /', $tester->getDisplay()); + } + /** * @return CommandTester */ From 25f1d85d6b9004b50990af525a67bf9bf721df99 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Wed, 30 Apr 2014 19:04:41 +0200 Subject: [PATCH 144/323] Fix issue 9172 --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/TranslationUpdateCommand.php | 8 +++++++ .../Component/Translation/CHANGELOG.md | 1 + .../Translation/Dumper/FileDumper.php | 21 ++++++++++++++++++- .../Translation/Writer/TranslationWriter.php | 10 +++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 9658d5729c648..25247608e59ea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Added `translation:debug` command + * Added `--no-backup` option to `translation:update` command * Added `config:debug` command * Added `yaml:lint` command * Deprecated the `RouterApacheDumperCommand` which will be removed in Symfony 3.0. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index b4c1e3efd7cc4..8457656f08a0d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -52,6 +52,10 @@ protected function configure() 'force', null, InputOption::VALUE_NONE, 'Should the update be done' ), + new InputOption( + 'no-backup', null, InputOption::VALUE_NONE, + 'Should backup be disabled' + ), new InputOption( 'clean', null, InputOption::VALUE_NONE, 'Should clean not found messages' @@ -139,6 +143,10 @@ protected function execute(InputInterface $input, OutputInterface $output) } } + if ($input->getOption('no-backup') === true) { + $writer->disableBackup(); + } + // save the files if ($input->getOption('force') === true) { $output->writeln('Writing files'); diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index 467feacd90b7b..8aedc370d4fd1 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added relative file path template to the file dumpers + * added optional backup to the file dumpers * changed IcuResFileDumper to extend FileDumper 2.3.0 diff --git a/src/Symfony/Component/Translation/Dumper/FileDumper.php b/src/Symfony/Component/Translation/Dumper/FileDumper.php index a70e995ddab54..7672400f02426 100644 --- a/src/Symfony/Component/Translation/Dumper/FileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/FileDumper.php @@ -31,6 +31,13 @@ abstract class FileDumper implements DumperInterface */ protected $relativePathTemplate = '%domain%.%locale%.%extension%'; + /** + * Make file backup before the dump. + * + * @var bool + */ + private $backup = true; + /** * Sets the template for the relative paths to files. * @@ -41,6 +48,16 @@ public function setRelativePathTemplate($relativePathTemplate) $this->relativePathTemplate = $relativePathTemplate; } + /** + * Sets backup flag. + * + * @param bool + */ + public function setBackup($backup) + { + $this->backup = $backup; + } + /** * {@inheritdoc} */ @@ -55,7 +72,9 @@ public function dump(MessageCatalogue $messages, $options = array()) // backup $fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale()); if (file_exists($fullpath)) { - copy($fullpath, $fullpath.'~'); + if ($this->backup) { + copy($fullpath, $fullpath.'~'); + } } else { $directory = dirname($fullpath); if (!file_exists($directory) && !@mkdir($directory, 0777, true)) { diff --git a/src/Symfony/Component/Translation/Writer/TranslationWriter.php b/src/Symfony/Component/Translation/Writer/TranslationWriter.php index 9d70c12f374a5..8d90797d113a7 100644 --- a/src/Symfony/Component/Translation/Writer/TranslationWriter.php +++ b/src/Symfony/Component/Translation/Writer/TranslationWriter.php @@ -39,6 +39,16 @@ public function addDumper($format, DumperInterface $dumper) $this->dumpers[$format] = $dumper; } + /** + * Disables dumper backup. + */ + public function disableBackup() + { + foreach ($this->dumpers as $dumper) { + $dumper->setBackup(false); + } + } + /** * Obtains the list of supported formats. * From 95f3276309ac7e3d939156f25fc2de0cdeff82a2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 5 May 2014 07:39:13 +0000 Subject: [PATCH 145/323] [Debug] fix handling deprecated warnings and stacked errors turned into exceptions --- src/Symfony/Component/Debug/ErrorHandler.php | 47 ++++++++++--------- .../Debug/Tests/ErrorHandlerTest.php | 6 +-- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index c6b71c36ab712..b62e5b752150f 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -154,12 +154,10 @@ function ($row) { self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack)); } - } - - return true; - } - if ($this->displayErrors && error_reporting() & $level && $this->level & $level) { + return true; + } + } elseif ($this->displayErrors && error_reporting() & $level && $this->level & $level) { if (self::$stackedErrorLevels) { self::$stackedErrors[] = func_get_args(); @@ -264,22 +262,35 @@ public function handleFatal() gc_collect_cycles(); $error = error_get_last(); - while (self::$stackedErrorLevels) { - static::unstackErrors(); - } + // get current exception handler + $exceptionHandler = set_exception_handler('var_dump'); + restore_exception_handler(); - if (null === $error) { - return; + try { + while (self::$stackedErrorLevels) { + static::unstackErrors(); + } + } catch (\Exception $exception) { + if ($exceptionHandler) { + call_user_func($exceptionHandler, $exception); + + return; + } + + if ($this->displayErrors) { + ini_set('display_errors', 1); + } + + throw $exception; } - $type = $error['type']; - if (0 === $this->level || !in_array($type, array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) { + if (!$error || !$this->level || !in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) { return; } if (isset(self::$loggers['emergency'])) { $fatal = array( - 'type' => $type, + 'type' => $error['type'], 'file' => $error['file'], 'line' => $error['line'], ); @@ -287,14 +298,8 @@ public function handleFatal() self::$loggers['emergency']->emergency($error['message'], $fatal); } - if ($this->displayErrors) { - // get current exception handler - $exceptionHandler = set_exception_handler('var_dump'); - restore_exception_handler(); - - if ($exceptionHandler || self::$fatalHandler) { - $this->handleFatalError($exceptionHandler, $error); - } + if ($this->displayErrors && ($exceptionHandler || self::$fatalHandler)) { + $this->handleFatalError($exceptionHandler, $error); } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 4c12a06d32f30..47652854042ae 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -134,12 +134,12 @@ public function testHandle() restore_error_handler(); $handler = ErrorHandler::register(E_USER_DEPRECATED); - $this->assertTrue($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array())); + $this->assertFalse($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array())); restore_error_handler(); $handler = ErrorHandler::register(E_DEPRECATED); - $this->assertTrue($handler->handle(E_DEPRECATED, 'foo', 'foo.php', 12, array())); + $this->assertFalse($handler->handle(E_DEPRECATED, 'foo', 'foo.php', 12, array())); restore_error_handler(); @@ -162,7 +162,7 @@ public function testHandle() $handler = ErrorHandler::register(E_USER_DEPRECATED); $handler->setLogger($logger); - $handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array()); + $this->assertTrue($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array())); restore_error_handler(); From 485e04749c5271560d332f1e8e749e407e4c805d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 6 May 2014 15:29:22 +0200 Subject: [PATCH 146/323] [Debug] enhance perf of DebugClassLoader --- .../Component/Debug/DebugClassLoader.php | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 2d768d37f89ed..eb1be7a46dd87 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -29,6 +29,7 @@ class DebugClassLoader private $classLoader; private $isFinder; private $wasFinder; + private static $caseCheck; /** * Constructor. @@ -49,6 +50,10 @@ public function __construct($classLoader) $this->classLoader = $classLoader; $this->isFinder = is_array($classLoader) && method_exists($classLoader[0], 'findFile'); } + + if (!isset(self::$caseCheck)) { + self::$caseCheck = false !== stripos(PHP_OS, 'win') ? (false !== stripos(PHP_OS, 'darwin') ? 2 : 1) : 0; + } } /** @@ -162,9 +167,13 @@ public function loadClass($class) $exists = class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false)); + if ('\\' === $class[0]) { + $class = substr($class, 1); + } + if ($exists) { - $name = new \ReflectionClass($class); - $name = $name->getName(); + $refl = new \ReflectionClass($class); + $name = $refl->getName(); if ($name !== $class) { throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name)); @@ -172,30 +181,35 @@ public function loadClass($class) } if ($file) { - if ('\\' == $class[0]) { - $class = substr($class, 1); - } + if (!$exists) { + if (false !== strpos($class, '/')) { + throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); + } - if (preg_match('#([/\\\\][a-zA-Z_\x7F-\xFF][a-zA-Z0-9_\x7F-\xFF]*)+\.(php|hh)$#', $file, $tail)) { + throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); + } + if (self::$caseCheck && preg_match('#([/\\\\][a-zA-Z_\x7F-\xFF][a-zA-Z0-9_\x7F-\xFF]*)+\.(php|hh)$#D', $file, $tail)) { $tail = $tail[0]; - $real = realpath($file); + $real = $refl->getFilename(); - if (false !== stripos(PHP_OS, 'darwin')) { - // realpath() on MacOSX doesn't normalize the case of characters, - // let's do it ourselves. This is tricky. + if (2 === self::$caseCheck) { + // realpath() on MacOSX doesn't normalize the case of characters $cwd = getcwd(); $basename = strrpos($real, '/'); chdir(substr($real, 0, $basename)); $basename = substr($real, $basename + 1); - $real = getcwd().'/'; - $h = opendir('.'); - while (false !== $f = readdir($h)) { - if (0 === strcasecmp($f, $basename)) { - $real .= $f; - break; + // glob() patterns are case-sensitive even if the underlying fs is not + if (!in_array($basename, glob($basename.'*', GLOB_NOSORT), true)) { + $real = getcwd().'/'; + $h = opendir('.'); + while (false !== $f = readdir($h)) { + if (0 === strcasecmp($f, $basename)) { + $real .= $f; + break; + } } + closedir($h); } - closedir($h); chdir($cwd); } @@ -206,14 +220,6 @@ public function loadClass($class) } } - if (!$exists) { - if (false !== strpos($class, '/')) { - throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); - } - - throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); - } - return true; } } From de2410a8c2190d970fe13365555e028eaf9988ab Mon Sep 17 00:00:00 2001 From: Joseph Bielawski Date: Fri, 9 May 2014 10:15:02 +0200 Subject: [PATCH 147/323] [Console] Make `Helper\Table::setStyle()` chainable Fixes #10874. --- src/Symfony/Component/Console/Helper/Table.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index 5bf7f55465e2f..5a3dbc168c0dd 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -123,6 +123,8 @@ public function setStyle($name) } else { throw new \InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } + + return $this; } /** From f800e150207eab1f5c933f507c5285c4359036a4 Mon Sep 17 00:00:00 2001 From: Ross Tuck Date: Fri, 9 May 2014 14:32:36 +0200 Subject: [PATCH 148/323] GraphizDumper now displays unresolved parameters Previously it would crash when given a container with an unresolved parameter. This change will instead show the parameter name on the final diagram, instead of the class name. --- .../DependencyInjection/Dumper/GraphvizDumper.php | 9 ++++++++- .../Tests/Dumper/GraphvizDumperTest.php | 8 ++++++++ .../Tests/Fixtures/containers/container17.php | 10 ++++++++++ .../Tests/Fixtures/graphviz/services17.dot | 8 ++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container17.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot diff --git a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php index 5ee1b7eea0fa1..9af912e8f04e8 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Dumper; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -164,8 +165,14 @@ private function findNodes() $container = $this->cloneContainer(); foreach ($container->getDefinitions() as $id => $definition) { - $nodes[$id] = array('class' => str_replace('\\', '\\\\', $this->container->getParameterBag()->resolveValue($definition->getClass())), 'attributes' => array_merge($this->options['node.definition'], array('style' => ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope() ? 'filled' : 'dotted'))); + $className = $definition->getClass(); + try { + $className = $this->container->getParameterBag()->resolveValue($className); + } catch (ParameterNotFoundException $e) { + } + + $nodes[$id] = array('class' => str_replace('\\', '\\\\', $className), 'attributes' => array_merge($this->options['node.definition'], array('style' => ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope() ? 'filled' : 'dotted'))); $container->setDefinition($id, new Definition('stdClass')); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php index 0dc1ce8de150d..b81c4c4b99d38 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php @@ -62,4 +62,12 @@ public function testDumpWithFrozenCustomClassContainer() $dumper = new GraphvizDumper($container); $this->assertEquals(str_replace('%path%', __DIR__, file_get_contents(self::$fixturesPath.'/graphviz/services14.dot')), $dumper->dump(), '->dump() dumps services'); } + + public function testDumpWithUnresolvedParameter() + { + $container = include self::$fixturesPath.'/containers/container17.php'; + $dumper = new GraphvizDumper($container); + + $this->assertEquals(str_replace('%path%', __DIR__, file_get_contents(self::$fixturesPath.'/graphviz/services17.dot')), $dumper->dump(), '->dump() dumps services'); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container17.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container17.php new file mode 100644 index 0000000000000..d902ec2a39306 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container17.php @@ -0,0 +1,10 @@ +register('foo', '%foo.class%') +; + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot new file mode 100644 index 0000000000000..a6d04bf5a097f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot @@ -0,0 +1,8 @@ +digraph sc { + ratio="compress" + node [fontsize="11" fontname="Arial" shape="record"]; + edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; + + node_foo [label="foo\n%foo.class%\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; +} From 0cc9746f87212f4ec712c37fb5d8e16e1d1db18b Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 9 May 2014 17:28:08 +0200 Subject: [PATCH 149/323] Fix issue #10867 --- .../PropertyAccess/PropertyAccessor.php | 13 ++- .../Tests/Fixtures/TestClass.php | 19 ++++ .../Tests/PropertyAccessorCollectionTest.php | 2 +- .../Tests/PropertyAccessorTest.php | 88 ++++++++++++------- 4 files changed, 85 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 44a4105c065b6..f10755f0f63de 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -314,12 +314,15 @@ private function &readProperty(&$object, $property) $camelProp = $this->camelize($property); $reflClass = new \ReflectionClass($object); $getter = 'get'.$camelProp; + $getter2 = lcfirst($camelProp); $isser = 'is'.$camelProp; $hasser = 'has'.$camelProp; $classHasProperty = $reflClass->hasProperty($property); if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) { $result[self::VALUE] = $object->$getter(); + } elseif ($this->isMethodAccessible($reflClass, $getter2, 0)) { + $result[self::VALUE] = $object->$getter2(); } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) { $result[self::VALUE] = $object->$isser(); } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) { @@ -341,7 +344,7 @@ private function &readProperty(&$object, $property) // we call the getter and hope the __call do the job $result[self::VALUE] = $object->$getter(); } else { - $methods = array($getter, $isser, $hasser, '__get'); + $methods = array($getter, $getter2, $isser, $hasser, '__get'); if ($this->magicCall) { $methods[] = '__call'; } @@ -413,10 +416,13 @@ private function writeProperty(&$object, $property, $value) } $setter = 'set'.$this->camelize($property); + $setter2 = lcfirst($plural); $classHasProperty = $reflClass->hasProperty($property); if ($this->isMethodAccessible($reflClass, $setter, 1)) { $object->$setter($value); + } elseif ($this->isMethodAccessible($reflClass, $setter2, 1)) { + $object->$setter2($value); } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) { $object->$property = $value; } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { @@ -433,13 +439,14 @@ private function writeProperty(&$object, $property, $value) $object->$setter($value); } else { throw new NoSuchPropertyException(sprintf( - 'Neither the property "%s" nor one of the methods %s"%s()", '. + 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '. '"__set()" or "__call()" exist and have public access in class "%s".', $property, implode('', array_map(function ($singular) { return '"add'.$singular.'()"/"remove'.$singular.'()", '; }, $singulars)), $setter, + $setter2, $reflClass->name )); } @@ -508,9 +515,11 @@ private function isPropertyWritable($object, $property) $reflClass = new \ReflectionClass($object); $setter = 'set'.$this->camelize($property); + $setter2 = lcfirst($this->camelize($property)); $classHasProperty = $reflClass->hasProperty($property); if ($this->isMethodAccessible($reflClass, $setter, 1) + || $this->isMethodAccessible($reflClass, $setter2, 1) || $this->isMethodAccessible($reflClass, '__set', 2) || ($classHasProperty && $reflClass->getProperty($property)->isPublic()) || (!$classHasProperty && property_exists($object, $property)) diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php index 9765c77da26c6..6b99251557b4e 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClass.php @@ -18,6 +18,8 @@ class TestClass private $privateProperty; private $publicAccessor; + private $publicMethodAccessor; + private $publicMethodMutator; private $publicAccessorWithDefaultValue; private $publicAccessorWithRequiredAndDefaultValue; private $publicAccessorWithMoreRequiredParameters; @@ -28,6 +30,8 @@ public function __construct($value) { $this->publicProperty = $value; $this->publicAccessor = $value; + $this->publicMethodAccessor = $value; + $this->publicMethodMutator = $value; $this->publicAccessorWithDefaultValue = $value; $this->publicAccessorWithRequiredAndDefaultValue = $value; $this->publicAccessorWithMoreRequiredParameters = $value; @@ -95,6 +99,21 @@ public function hasPublicHasAccessor() return $this->publicHasAccessor; } + public function publicMethodAccessor() + { + return $this->publicMethodAccessor; + } + + public function publicMethodMutator($value) + { + $this->publicMethodMutator = $value; + } + + public function getPublicMethodMutator() + { + return $this->publicMethodMutator; + } + protected function setProtectedAccessor($value) { } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 7e4a49247c09e..60bd9dead870e 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -172,7 +172,7 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()"/"removeAx()", "addAxe()"/"removeAxe()", "addAxis()"/"removeAxis()", "setAxes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover + * @expectedExceptionMessage Neither the property "axes" nor one of the methods "addAx()"/"removeAx()", "addAxe()"/"removeAxe()", "addAxis()"/"removeAxis()", "setAxes()", "axes()", "__set()" or "__call()" exist and have public access in class "Mock_PropertyAccessorCollectionTest_CarNoAdderAndRemover */ public function testSetValueFailsIfNoAdderNorRemoverFound() { diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 6fc5f7022f525..4ddf10234bc8d 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -28,38 +28,23 @@ protected function setUp() $this->propertyAccessor = new PropertyAccessor(); } - public function getValidPropertyPaths() + public function getValidGetPropertyPaths() { - return array( - array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'), - array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'), - array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), - array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'), - array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'), - array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'), - array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'), - array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'), - - // Accessor methods - array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'), - array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'), - array(new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'), - array(new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'), - array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'), - array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'), - - // Methods are camelized - array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'), - - // Missing indices - array(array('index' => array()), '[index][firstName]', null), - array(array('root' => array('index' => array())), '[root][index][firstName]', null), + return array_merge( + array( + array(new TestClass('Bernhard'), 'publicMethodAccessor', 'Bernhard', 'Bernhard'), + ), + $this->getValidPropertyPaths() + ); + } - // Special chars - array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'), - array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'), - array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'), - array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'), + public function getValidSetPropertyPaths() + { + return array_merge( + array( + array(new TestClass('Bernhard'), 'publicMethodMutator', 'Bernhard', 'Bernhard'), + ), + $this->getValidPropertyPaths() ); } @@ -95,7 +80,7 @@ public function getPathsWithMissingIndex() } /** - * @dataProvider getValidPropertyPaths + * @dataProvider getValidGetPropertyPaths */ public function testGetValue($objectOrArray, $path, $value) { @@ -196,7 +181,7 @@ public function testGetValueThrowsExceptionIfEmpty() } /** - * @dataProvider getValidPropertyPaths + * @dataProvider getValidSetPropertyPaths */ public function testSetValue($objectOrArray, $path) { @@ -312,7 +297,7 @@ public function testSetValueThrowsExceptionIfEmpty() } /** - * @dataProvider getValidPropertyPaths + * @dataProvider getValidGetPropertyPaths */ public function testIsReadable($objectOrArray, $path) { @@ -380,7 +365,7 @@ public function testIsReadableThrowsExceptionIfEmpty() } /** - * @dataProvider getValidPropertyPaths + * @dataProvider getValidSetPropertyPaths */ public function testIsWritable($objectOrArray, $path) { @@ -446,4 +431,39 @@ public function testIsWritableThrowsExceptionIfEmpty() { $this->assertFalse($this->propertyAccessor->isWritable('', 'foobar', 'Updated')); } + + private function getValidPropertyPaths() + { + return array( + array(array('Bernhard', 'Schussek'), '[0]', 'Bernhard'), + array(array('Bernhard', 'Schussek'), '[1]', 'Schussek'), + array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), + array(array('index' => array('firstName' => 'Bernhard')), '[index][firstName]', 'Bernhard'), + array((object) array('firstName' => 'Bernhard'), 'firstName', 'Bernhard'), + array((object) array('property' => array('firstName' => 'Bernhard')), 'property[firstName]', 'Bernhard'), + array(array('index' => (object) array('firstName' => 'Bernhard')), '[index].firstName', 'Bernhard'), + array((object) array('property' => (object) array('firstName' => 'Bernhard')), 'property.firstName', 'Bernhard'), + + // Accessor methods + array(new TestClass('Bernhard'), 'publicProperty', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessorWithDefaultValue', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicAccessorWithRequiredAndDefaultValue', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicIsAccessor', 'Bernhard'), + array(new TestClass('Bernhard'), 'publicHasAccessor', 'Bernhard'), + + // Methods are camelized + array(new TestClass('Bernhard'), 'public_accessor', 'Bernhard'), + + // Missing indices + array(array('index' => array()), '[index][firstName]', null), + array(array('root' => array('index' => array())), '[root][index][firstName]', null), + + // Special chars + array(array('%!@$§.' => 'Bernhard'), '[%!@$§.]', 'Bernhard'), + array(array('index' => array('%!@$§.' => 'Bernhard')), '[index][%!@$§.]', 'Bernhard'), + array((object) array('%!@$§' => 'Bernhard'), '%!@$§', 'Bernhard'), + array((object) array('property' => (object) array('%!@$§' => 'Bernhard')), 'property.%!@$§', 'Bernhard'), + ); + } } From af999971b42536b512ba0c0d2abf00213d273671 Mon Sep 17 00:00:00 2001 From: t3chn0r Date: Wed, 14 May 2014 09:24:58 -0400 Subject: [PATCH 150/323] Update MessageSelector.php to show which value was passed When the translator can't find a correct match using transChoice() a string is logged today with the following text: Unable to choose a translation for "" with locale "". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples"). This change introduces the value that was passed to transChoice() to the developer will have more information as to what value did not match any of the translation options: Unable to choose a translation for "" with locale "" for value "". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples"). --- src/Symfony/Component/Translation/MessageSelector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/MessageSelector.php b/src/Symfony/Component/Translation/MessageSelector.php index 1802d16e27e78..cdf814e45e922 100644 --- a/src/Symfony/Component/Translation/MessageSelector.php +++ b/src/Symfony/Component/Translation/MessageSelector.php @@ -82,7 +82,7 @@ public function choose($message, $number, $locale) return $standardRules[0]; } - throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale)); + throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale, $number)); } return $standardRules[$position]; From 8e2d69e458ede5535212949079601ac4178fc2ee Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Thu, 15 May 2014 09:56:35 +0100 Subject: [PATCH 151/323] [Console] Update test fixtures to match the new way we handle colors. --- ...plication_renderexception_doublewidth1decorated.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt index c68a60f564df0..8c8801b13ff2b 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_renderexception_doublewidth1decorated.txt @@ -1,11 +1,11 @@ -  - [Exception]  - エラーメッセージ  -  +  + [Exception]  + エラーメッセージ  +  -foo +foo From 951acca387fe4d06684a10eee5787e9d1524d923 Mon Sep 17 00:00:00 2001 From: sun Date: Thu, 15 May 2014 02:05:21 +0200 Subject: [PATCH 152/323] Fixed YAML Parser does not ignore duplicate keys, violating YAML spec. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current [YAML 1.2] specification clearly states: > JSON's RFC4627 requires that mappings keys merely “SHOULD” be unique, while YAML insists they “MUST” be. The outdated [YAML 1.1] spec contained a crystal clear note on how the error of duplicate keys is to be handled by parsers, which is (sadly) no longer contained in the latest 1.2 spec (only leaving the requirement): > It is an error for two equal keys to appear in the same mapping node. In such a case the YAML processor may continue, ignoring the second `key: value` pair and issuing an appropriate warning. This strategy preserves a consistent information model for one-pass and random access applications. [YAML 1.2]: http://yaml.org/spec/1.2/spec.html#id2759572 [YAML 1.1]: http://yaml.org/spec/1.1/#id932806 --- src/Symfony/Component/Yaml/Inline.php | 24 ++++++++-- src/Symfony/Component/Yaml/Parser.php | 23 +++++++-- .../Component/Yaml/Tests/ParserTest.php | 47 +++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index b0d6a031f8b83..2242d12c7be48 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -350,19 +350,37 @@ private static function parseMapping($mapping, &$i = 0) switch ($mapping[$i]) { case '[': // nested sequence - $output[$key] = self::parseSequence($mapping, $i); + $value = self::parseSequence($mapping, $i); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($output[$key])) { + $output[$key] = $value; + } $done = true; break; case '{': // nested mapping - $output[$key] = self::parseMapping($mapping, $i); + $value = self::parseMapping($mapping, $i); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($output[$key])) { + $output[$key] = $value; + } $done = true; break; case ':': case ' ': break; default: - $output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); + $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($output[$key])) { + $output[$key] = $value; + } $done = true; --$i; } diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 9bde67b57b548..9539e36aa3c7f 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -178,18 +178,35 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { - $data[$key] = null; + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($data[$key])) { + $data[$key] = null; + } } else { $c = $this->getRealCurrentLineNb() + 1; $parser = new Parser($c); $parser->refs =& $this->refs; - $data[$key] = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); + $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($data[$key])) { + $data[$key] = $value; + } } } else { if ($isInPlace) { $data = $this->refs[$isInPlace]; } else { - $data[$key] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport); + $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport);; + // Spec: Keys MUST be unique; first one wins. + // Parser cannot abort this mapping earlier, since lines + // are processed sequentially. + if (!isset($data[$key])) { + $data[$key] = $value; + } } } } else { diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 2335efc55990f..a4db960729e01 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -508,6 +508,53 @@ public function testMappingInASequence() ); } + /** + * > It is an error for two equal keys to appear in the same mapping node. + * > In such a case the YAML processor may continue, ignoring the second + * > `key: value` pair and issuing an appropriate warning. This strategy + * > preserves a consistent information model for one-pass and random access + * > applications. + * + * @see http://yaml.org/spec/1.2/spec.html#id2759572 + * @see http://yaml.org/spec/1.1/#id932806 + * + * @covers \Symfony\Component\Yaml\Parser::parse + */ + public function testMappingDuplicateKeyBlock() + { + $input = << array( + 'child' => 'first', + ), + ); + $this->assertSame($expected, Yaml::parse($input)); + } + + /** + * @covers \Symfony\Component\Yaml\Inline::parseMapping + */ + public function testMappingDuplicateKeyFlow() + { + $input = << array( + 'child' => 'first', + ), + ); + $this->assertSame($expected, Yaml::parse($input)); + } + public function testEmptyValue() { $input = << Date: Fri, 16 May 2014 16:25:29 +0200 Subject: [PATCH 153/323] [Validator] Updated outdated dependencies. --- src/Symfony/Component/Validator/composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 51266b61869a6..012697b19b3c2 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -27,7 +27,8 @@ "symfony/property-access": "~2.2", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", - "egulias/email-validator": "~1.0" + "egulias/email-validator": "~1.0", + "symfony/expression-language": "~2.4" }, "suggest": { "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", @@ -37,7 +38,8 @@ "symfony/yaml": "", "symfony/config": "", "egulias/email-validator": "Strict (RFC compliant) email validation", - "symfony/property-access": "For using the 2.4 Validator API" + "symfony/property-access": "For using the 2.4 Validator API", + "symfony/expression-language": "For using the 2.4 Expression validator" }, "autoload": { "psr-0": { "Symfony\\Component\\Validator\\": "" } From 86f9cb90eb8ac83801e3075c96b504a87e73b1e6 Mon Sep 17 00:00:00 2001 From: Charles Sarrazin Date: Fri, 16 May 2014 14:30:08 +0200 Subject: [PATCH 154/323] Added support for injecting HttpFoundation's RequestStack in ServerParams --- .../Extension/Validator/Util/ServerParams.php | 13 ++++++++++ .../Validator/Util/ServerParamsTest.php | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php b/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php index 93a1dd7d6fe2c..6623edd56852d 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php +++ b/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php @@ -11,11 +11,20 @@ namespace Symfony\Component\Form\Extension\Validator\Util; +use Symfony\Component\HttpFoundation\RequestStack; + /** * @author Bernhard Schussek */ class ServerParams { + private $requestStack; + + public function __construct(RequestStack $requestStack = null) + { + $this->requestStack = $requestStack; + } + /** * Returns maximum post size in bytes. * @@ -65,6 +74,10 @@ public function getNormalizedIniPostMaxSize() */ public function getContentLength() { + if (null !== $this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) { + return $request->server->get('CONTENT_LENGTH'); + } + return isset($_SERVER['CONTENT_LENGTH']) ? (int) $_SERVER['CONTENT_LENGTH'] : null; diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Util/ServerParamsTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Util/ServerParamsTest.php index 7ad5b77106a1c..957b59702e0d2 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Util/ServerParamsTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Util/ServerParamsTest.php @@ -11,8 +11,33 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Util; +use Symfony\Component\Form\Extension\Validator\Util\ServerParams; +use Symfony\Component\HttpFoundation\Request; + class ServerParamsTest extends \PHPUnit_Framework_TestCase { + public function testGetContentLengthFromSuperglobals() + { + $serverParams = new ServerParams(); + $this->assertNull($serverParams->getContentLength()); + + $_SERVER['CONTENT_LENGTH'] = 1024; + + $this->assertEquals(1024, $serverParams->getContentLength()); + + unset($_SERVER['CONTENT_LENGTH']); + } + + public function testGetContentLengthFromRequest() + { + $request = Request::create('http://foo', 'GET', array(), array(), array(), array('CONTENT_LENGTH' => 1024)); + $requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array('getCurrentRequest')); + $requestStack->expects($this->once())->method('getCurrentRequest')->will($this->returnValue($request)); + $serverParams = new ServerParams($requestStack); + + $this->assertEquals(1024, $serverParams->getContentLength()); + } + /** @dataProvider getGetPostMaxSizeTestData */ public function testGetPostMaxSize($size, $bytes) { From f9f385252df7f5b8ac48fd27db2d49de9de90e92 Mon Sep 17 00:00:00 2001 From: "Issei.M" Date: Sat, 17 May 2014 17:07:33 +0900 Subject: [PATCH 155/323] [Security] removed an unused parameter in some private methods --- .../Http/Firewall/AbstractAuthenticationListener.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php index 80bfcd0b96dcd..cc1c4a10c201f 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php @@ -149,14 +149,14 @@ final public function handle(GetResponseEvent $event) if ($returnValue instanceof TokenInterface) { $this->sessionStrategy->onAuthentication($request, $returnValue); - $response = $this->onSuccess($event, $request, $returnValue); + $response = $this->onSuccess($request, $returnValue); } elseif ($returnValue instanceof Response) { $response = $returnValue; } else { throw new \RuntimeException('attemptAuthentication() must either return a Response, an implementation of TokenInterface, or null.'); } } catch (AuthenticationException $e) { - $response = $this->onFailure($event, $request, $e); + $response = $this->onFailure($request, $e); } $event->setResponse($response); @@ -189,7 +189,7 @@ protected function requiresAuthentication(Request $request) */ abstract protected function attemptAuthentication(Request $request); - private function onFailure(GetResponseEvent $event, Request $request, AuthenticationException $failed) + private function onFailure(Request $request, AuthenticationException $failed) { if (null !== $this->logger) { $this->logger->info(sprintf('Authentication request failed: %s', $failed->getMessage())); @@ -209,7 +209,7 @@ private function onFailure(GetResponseEvent $event, Request $request, Authentica return $response; } - private function onSuccess(GetResponseEvent $event, Request $request, TokenInterface $token) + private function onSuccess(Request $request, TokenInterface $token) { if (null !== $this->logger) { $this->logger->info(sprintf('User "%s" has been authenticated successfully', $token->getUsername())); From f416e7044ce4b5a2f329b188db17e404b2932a71 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Sat, 17 May 2014 15:22:00 +0200 Subject: [PATCH 156/323] [DomCrawler] Changed typehints form DomNode to DomElement Closes #10924 --- src/Symfony/Component/DomCrawler/CHANGELOG.md | 7 +++++++ src/Symfony/Component/DomCrawler/Crawler.php | 2 +- .../DomCrawler/Field/ChoiceFormField.php | 8 ++++---- .../Component/DomCrawler/Field/FormField.php | 6 +++--- src/Symfony/Component/DomCrawler/Form.php | 20 +++++++++---------- src/Symfony/Component/DomCrawler/Link.php | 18 ++++++++--------- .../DomCrawler/Tests/CrawlerTest.php | 4 ++-- .../Tests/Field/ChoiceFormFieldTest.php | 2 +- 8 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md index bc5180513360d..b60ac0140e9b2 100644 --- a/src/Symfony/Component/DomCrawler/CHANGELOG.md +++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +2.5.0 +----- + +* [BC BREAK] The typehints on the `Link`, `Form` and `FormField` classes have been changed from + `\DOMNode` to `DOMElement`. Using any other type of `DOMNode` was triggering fatal errors in previous + versions. Code extending these classes will need to update the typehints when overwriting these methods. + 2.4.0 ----- diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 7c54397240dc9..5f84691c69369 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -14,7 +14,7 @@ use Symfony\Component\CssSelector\CssSelector; /** - * Crawler eases navigation of a list of \DOMNode objects. + * Crawler eases navigation of a list of \DOMElement objects. * * @author Fabien Potencier * diff --git a/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php b/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php index 934b7e7133112..8575e7216f3e6 100644 --- a/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php +++ b/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php @@ -161,11 +161,11 @@ public function setValue($value) * * This method should only be used internally. * - * @param \DOMNode $node A \DOMNode + * @param \DOMElement $node * * @throws \LogicException When choice provided is not multiple nor radio */ - public function addChoice(\DOMNode $node) + public function addChoice(\DOMElement $node) { if (!$this->multiple && 'radio' != $this->type) { throw new \LogicException(sprintf('Unable to add a choice for "%s" as it is not multiple or is not a radio button.', $this->name)); @@ -259,11 +259,11 @@ protected function initialize() /** * Returns option value with associated disabled flag * - * @param \DOMNode $node + * @param \DOMElement $node * * @return array */ - private function buildOptionValue($node) + private function buildOptionValue(\DOMElement $node) { $option = array(); diff --git a/src/Symfony/Component/DomCrawler/Field/FormField.php b/src/Symfony/Component/DomCrawler/Field/FormField.php index 2114b4ed5de16..efad8bf6b594a 100644 --- a/src/Symfony/Component/DomCrawler/Field/FormField.php +++ b/src/Symfony/Component/DomCrawler/Field/FormField.php @@ -19,7 +19,7 @@ abstract class FormField { /** - * @var \DOMNode + * @var \DOMElement */ protected $node; /** @@ -46,9 +46,9 @@ abstract class FormField /** * Constructor. * - * @param \DOMNode $node The node associated with this field + * @param \DOMElement $node The node associated with this field */ - public function __construct(\DOMNode $node) + public function __construct(\DOMElement $node) { $this->node = $node; $this->name = $node->getAttribute('name'); diff --git a/src/Symfony/Component/DomCrawler/Form.php b/src/Symfony/Component/DomCrawler/Form.php index 46d4ad4cb14c5..adce403f07b02 100644 --- a/src/Symfony/Component/DomCrawler/Form.php +++ b/src/Symfony/Component/DomCrawler/Form.php @@ -23,7 +23,7 @@ class Form extends Link implements \ArrayAccess { /** - * @var \DOMNode + * @var \DOMElement */ private $button; @@ -35,15 +35,15 @@ class Form extends Link implements \ArrayAccess /** * Constructor. * - * @param \DOMNode $node A \DOMNode instance - * @param string $currentUri The URI of the page where the form is embedded - * @param string $method The method to use for the link (if null, it defaults to the method defined by the form) + * @param \DOMElement $node A \DOMElement instance + * @param string $currentUri The URI of the page where the form is embedded + * @param string $method The method to use for the link (if null, it defaults to the method defined by the form) * * @throws \LogicException if the node is not a button inside a form tag * * @api */ - public function __construct(\DOMNode $node, $currentUri, $method = null) + public function __construct(\DOMElement $node, $currentUri, $method = null) { parent::__construct($node, $currentUri, $method); @@ -53,7 +53,7 @@ public function __construct(\DOMNode $node, $currentUri, $method = null) /** * Gets the form node associated with this form. * - * @return \DOMNode A \DOMNode instance + * @return \DOMElement A \DOMElement instance */ public function getFormNode() { @@ -359,13 +359,13 @@ public function disableValidation() /** * Sets the node for the form. * - * Expects a 'submit' button \DOMNode and finds the corresponding form element. + * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself. * - * @param \DOMNode $node A \DOMNode instance + * @param \DOMElement $node A \DOMElement instance * * @throws \LogicException If given node is not a button or input or does not have a form ancestor */ - protected function setNode(\DOMNode $node) + protected function setNode(\DOMElement $node) { $this->button = $node; if ('button' == $node->nodeName || ('input' == $node->nodeName && in_array(strtolower($node->getAttribute('type')), array('submit', 'button', 'image')))) { @@ -454,7 +454,7 @@ private function initialize() } } - private function addField(\DOMNode $node) + private function addField(\DOMElement $node) { if (!$node->hasAttribute('name') || !$node->getAttribute('name')) { return; diff --git a/src/Symfony/Component/DomCrawler/Link.php b/src/Symfony/Component/DomCrawler/Link.php index d662f8937c855..96e9ec3134b26 100644 --- a/src/Symfony/Component/DomCrawler/Link.php +++ b/src/Symfony/Component/DomCrawler/Link.php @@ -21,7 +21,7 @@ class Link { /** - * @var \DOMNode A \DOMNode instance + * @var \DOMElement */ protected $node; /** @@ -36,15 +36,15 @@ class Link /** * Constructor. * - * @param \DOMNode $node A \DOMNode instance - * @param string $currentUri The URI of the page where the link is embedded (or the base href) - * @param string $method The method to use for the link (get by default) + * @param \DOMElement $node A \DOMElement instance + * @param string $currentUri The URI of the page where the link is embedded (or the base href) + * @param string $method The method to use for the link (get by default) * * @throws \InvalidArgumentException if the node is not a link * * @api */ - public function __construct(\DOMNode $node, $currentUri, $method = 'GET') + public function __construct(\DOMElement $node, $currentUri, $method = 'GET') { if (!in_array(strtolower(substr($currentUri, 0, 4)), array('http', 'file'))) { throw new \InvalidArgumentException(sprintf('Current URI must be an absolute URL ("%s").', $currentUri)); @@ -58,7 +58,7 @@ public function __construct(\DOMNode $node, $currentUri, $method = 'GET') /** * Gets the node associated with this link. * - * @return \DOMNode A \DOMNode instance + * @return \DOMElement A \DOMElement instance */ public function getNode() { @@ -180,13 +180,13 @@ protected function canonicalizePath($path) } /** - * Sets current \DOMNode instance. + * Sets current \DOMElement instance. * - * @param \DOMNode $node A \DOMNode instance + * @param \DOMElement $node A \DOMElement instance * * @throws \LogicException If given node is not an anchor */ - protected function setNode(\DOMNode $node) + protected function setNode(\DOMElement $node) { if ('a' != $node->nodeName && 'area' != $node->nodeName) { throw new \LogicException(sprintf('Unable to click on a "%s" tag.', $node->nodeName)); diff --git a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php index aa3082a23eae2..cdc5adebffdf8 100644 --- a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php @@ -47,7 +47,7 @@ public function testAdd() $crawler = new Crawler(); $crawler->add($this->createNodeList()->item(0)); - $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->add() adds nodes from an \DOMNode'); + $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->add() adds nodes from a \DOMElement'); $crawler = new Crawler(); $crawler->add('Foo'); @@ -280,7 +280,7 @@ public function testAddNode() $crawler = new Crawler(); $crawler->addNode($this->createNodeList()->item(0)); - $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addNode() adds nodes from an \DOMNode'); + $this->assertEquals('foo', $crawler->filterXPath('//div')->attr('class'), '->addNode() adds nodes from a \DOMElement'); } public function testClear() diff --git a/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php b/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php index d2a95a53e8611..f0b086816e8bf 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php @@ -191,7 +191,7 @@ public function testCheckboxes() $this->assertNull($field->getValue(), '->getValue() returns null if the checkbox is not checked'); $this->assertFalse($field->isMultiple(), '->hasValue() returns false for checkboxes'); try { - $field->addChoice(new \DOMNode()); + $field->addChoice(new \DOMElement('input')); $this->fail('->addChoice() throws a \LogicException for checkboxes'); } catch (\LogicException $e) { $this->assertTrue(true, '->initialize() throws a \LogicException for checkboxes'); From 186b65ea5d8fad1b4654aa77cbd71dc15edcd700 Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Sat, 17 May 2014 22:04:22 +0200 Subject: [PATCH 157/323] Changed the default value of checkbox and radio to match the HTML spec When the checkbox or radio input does not have a value attribute, the HTML spec defines that the value should be 'on', not '1'. --- src/Symfony/Component/DomCrawler/CHANGELOG.md | 2 ++ src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php | 2 +- .../DomCrawler/Tests/Field/ChoiceFormFieldTest.php | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md index b60ac0140e9b2..48fd323f8202c 100644 --- a/src/Symfony/Component/DomCrawler/CHANGELOG.md +++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 2.5.0 ----- +* [BC BREAK] The default value for checkbox and radio inputs without a value attribute have changed + from '1' to 'on' to match the HTML specification. * [BC BREAK] The typehints on the `Link`, `Form` and `FormField` classes have been changed from `\DOMNode` to `DOMElement`. Using any other type of `DOMNode` was triggering fatal errors in previous versions. Code extending these classes will need to update the typehints when overwriting these methods. diff --git a/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php b/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php index 8575e7216f3e6..7ccc4fa15b1bb 100644 --- a/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php +++ b/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php @@ -267,7 +267,7 @@ private function buildOptionValue(\DOMElement $node) { $option = array(); - $defaultValue = (isset($node->nodeValue) && !empty($node->nodeValue)) ? $node->nodeValue : '1'; + $defaultValue = (isset($node->nodeValue) && !empty($node->nodeValue)) ? $node->nodeValue : 'on'; $option['value'] = $node->hasAttribute('value') ? $node->getAttribute('value') : $defaultValue; $option['disabled'] = ($node->hasAttribute('disabled') && $node->getAttribute('disabled') == 'disabled'); diff --git a/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php b/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php index f0b086816e8bf..09deb21370875 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php @@ -201,7 +201,7 @@ public function testCheckboxes() $field = new ChoiceFormField($node); $this->assertTrue($field->hasValue(), '->hasValue() returns true when the checkbox is checked'); - $this->assertEquals('1', $field->getValue(), '->getValue() returns 1 if the checkbox is checked and has no value attribute'); + $this->assertEquals('on', $field->getValue(), '->getValue() returns 1 if the checkbox is checked and has no value attribute'); $node = $this->createNode('input', '', array('type' => 'checkbox', 'name' => 'name', 'checked' => 'checked', 'value' => 'foo')); $field = new ChoiceFormField($node); @@ -240,7 +240,7 @@ public function testTick() $node = $this->createNode('input', '', array('type' => 'checkbox', 'name' => 'name')); $field = new ChoiceFormField($node); $field->tick(); - $this->assertEquals(1, $field->getValue(), '->tick() ticks checkboxes'); + $this->assertEquals('on', $field->getValue(), '->tick() ticks checkboxes'); } public function testUntick() @@ -266,7 +266,7 @@ public function testSelect() $node = $this->createNode('input', '', array('type' => 'checkbox', 'name' => 'name', 'checked' => 'checked')); $field = new ChoiceFormField($node); $field->select(true); - $this->assertEquals(1, $field->getValue(), '->select() changes the value of the field'); + $this->assertEquals('on', $field->getValue(), '->select() changes the value of the field'); $field->select(false); $this->assertNull($field->getValue(), '->select() changes the value of the field'); From dfa8ff87f3cff4f33d9d003814bf4d9043c9ee1b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 19 May 2014 12:15:59 +0200 Subject: [PATCH 158/323] [Debug] cleanup interfaces before 2.5-final --- .../Resources/config/debug.xml | 6 +- src/Symfony/Component/Debug/CHANGELOG.md | 3 +- src/Symfony/Component/Debug/ErrorHandler.php | 89 ++--------------- .../Component/Debug/ExceptionHandler.php | 95 +++++++++++++++++-- .../Debug/ExceptionHandlerInterface.php | 29 ------ .../EventListener/DebugHandlersListener.php | 50 ++++++++++ .../FatalErrorExceptionsListener.php | 47 --------- .../Component/HttpKernel/HttpKernel.php | 10 +- 8 files changed, 156 insertions(+), 173 deletions(-) delete mode 100644 src/Symfony/Component/Debug/ExceptionHandlerInterface.php create mode 100644 src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php delete mode 100644 src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index 2366ac1f0604e..c457e4f903a36 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -9,7 +9,7 @@ Symfony\Component\Stopwatch\Stopwatch %kernel.cache_dir%/%kernel.container_class%.xml Symfony\Component\HttpKernel\Controller\TraceableControllerResolver - Symfony\Component\HttpKernel\EventListener\FatalErrorExceptionsListener + Symfony\Component\HttpKernel\EventListener\DebugHandlersListener @@ -41,11 +41,11 @@ - + - handleFatalErrorException + terminateWithException diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index b128efaaa8913..776468fb7a59e 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -4,9 +4,8 @@ CHANGELOG 2.5.0 ----- -* added ErrorHandler::setFatalErrorExceptionHandler() +* added ExceptionHandler::setHandler() * added UndefinedMethodFatalErrorHandler -* deprecated ExceptionHandlerInterface * deprecated DummyException 2.4.0 diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index b62e5b752150f..9a6f6fa1c3699 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -53,8 +53,6 @@ class ErrorHandler private $displayErrors; - private $caughtOutput = 0; - /** * @var LoggerInterface[] Loggers for channels */ @@ -64,8 +62,6 @@ class ErrorHandler private static $stackedErrorLevels = array(); - private static $fatalHandler = false; - /** * Registers the error handler. * @@ -119,16 +115,6 @@ public static function setLogger(LoggerInterface $logger, $channel = 'deprecatio self::$loggers[$channel] = $logger; } - /** - * Sets a fatal error exception handler. - * - * @param callable $handler An handler that will be called on FatalErrorException - */ - public static function setFatalErrorExceptionHandler($handler) - { - self::$fatalHandler = $handler; - } - /** * @throws ContextErrorException When error_reporting returns error */ @@ -284,7 +270,7 @@ public function handleFatal() throw $exception; } - if (!$error || !$this->level || !in_array($error['type'], array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) { + if (!$error || !$this->level || !($error['type'] & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_PARSE))) { return; } @@ -298,7 +284,7 @@ public function handleFatal() self::$loggers['emergency']->emergency($error['message'], $fatal); } - if ($this->displayErrors && ($exceptionHandler || self::$fatalHandler)) { + if ($this->displayErrors && $exceptionHandler) { $this->handleFatalError($exceptionHandler, $error); } } @@ -336,73 +322,12 @@ private function handleFatalError($exceptionHandler, array $error) } } - // To be as fail-safe as possible, the FatalErrorException is first handled - // by the exception handler, then by the fatal error handler. The latter takes - // precedence and any output from the former is cancelled, if and only if - // nothing bad happens in this handling path. - - $caughtOutput = 0; - - if ($exceptionHandler) { - $this->caughtOutput = false; - ob_start(array($this, 'catchOutput')); - try { - call_user_func($exceptionHandler, $exception); - } catch (\Exception $e) { - // Ignore this exception, we have to deal with the fatal error - } - if (false === $this->caughtOutput) { - ob_end_clean(); - } - if (isset($this->caughtOutput[0])) { - ob_start(array($this, 'cleanOutput')); - echo $this->caughtOutput; - $caughtOutput = ob_get_length(); - } - $this->caughtOutput = 0; - } - - if (self::$fatalHandler) { - try { - call_user_func(self::$fatalHandler, $exception); - - if ($caughtOutput) { - $this->caughtOutput = $caughtOutput; - } - } catch (\Exception $e) { - if (!$caughtOutput) { - // Neither the exception nor the fatal handler succeeded. - // Let PHP handle that now. - throw $exception; - } - } - } - } - - /** - * @internal - */ - public function catchOutput($buffer) - { - $this->caughtOutput = $buffer; - - return ''; - } - - /** - * @internal - */ - public function cleanOutput($buffer) - { - if ($this->caughtOutput) { - // use substr_replace() instead of substr() for mbstring overloading resistance - $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtOutput); - if (isset($cleanBuffer[0])) { - $buffer = $cleanBuffer; - } + try { + call_user_func($exceptionHandler, $exception); + } catch (\Exception $e) { + // The handler failed. Let PHP handle that now. + throw $exception; } - - return $buffer; } } diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index 91e904fbe25ce..4979899afcdfb 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -29,10 +29,12 @@ * * @author Fabien Potencier */ -class ExceptionHandler implements ExceptionHandlerInterface +class ExceptionHandler { private $debug; private $charset; + private $handler; + private $caughtOutput = 0; public function __construct($debug = true, $charset = 'UTF-8') { @@ -56,6 +58,22 @@ public static function register($debug = true) return $handler; } + /** + * Sets a user exception handler. + * + * @param callable $handler An handler that will be called on Exception + */ + public function setHandler($handler) + { + if (isset($handler) && !is_callable($handler)) { + throw new \LogicException('The exception handler must be a valid PHP callable.'); + } + $old = $this->handler; + $this->handler = $handler; + + return $old; + } + /** * {@inheritdoc} * @@ -70,12 +88,49 @@ public static function register($debug = true) */ public function handle(\Exception $exception) { - if (class_exists('Symfony\Component\HttpFoundation\Response')) { - $response = $this->createResponse($exception); - $response->sendHeaders(); - $response->sendContent(); - } else { - $this->sendPhpResponse($exception); + // To be as fail-safe as possible, the exception is first handled + // by our simple exception handler, then by the user exception handler. + // The latter takes precedence and any output from the former is cancelled, + // if and only if nothing bad happens in this handling path. + + $caughtOutput = 0; + + $this->caughtOutput = false; + ob_start(array($this, 'catchOutput')); + try { + if (class_exists('Symfony\Component\HttpFoundation\Response')) { + $response = $this->createResponse($exception); + $response->sendHeaders(); + $response->sendContent(); + } else { + $this->sendPhpResponse($exception); + } + } catch (\Exception $e) { + // Ignore this $e exception, we have to deal with $exception + } + if (false === $this->caughtOutput) { + ob_end_clean(); + } + if (isset($this->caughtOutput[0])) { + ob_start(array($this, 'cleanOutput')); + echo $this->caughtOutput; + $caughtOutput = ob_get_length(); + } + $this->caughtOutput = 0; + + if (!empty($this->handler)) { + try { + call_user_func($this->handler, $exception); + + if ($caughtOutput) { + $this->caughtOutput = $caughtOutput; + } + } catch (\Exception $e) { + if (!$caughtOutput) { + // All handlers failed. Let PHP handle that now. + throw $exception; + } + } } } @@ -317,4 +372,30 @@ private function formatArgs(array $args) return implode(', ', $result); } + + /** + * @internal + */ + public function catchOutput($buffer) + { + $this->caughtOutput = $buffer; + + return ''; + } + + /** + * @internal + */ + public function cleanOutput($buffer) + { + if ($this->caughtOutput) { + // use substr_replace() instead of substr() for mbstring overloading resistance + $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtOutput); + if (isset($cleanBuffer[0])) { + $buffer = $cleanBuffer; + } + } + + return $buffer; + } } diff --git a/src/Symfony/Component/Debug/ExceptionHandlerInterface.php b/src/Symfony/Component/Debug/ExceptionHandlerInterface.php deleted file mode 100644 index f1740184c6dfe..0000000000000 --- a/src/Symfony/Component/Debug/ExceptionHandlerInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Debug; - -/** - * An ExceptionHandler does something useful with an exception. - * - * @author Andrew Moore - * - * @deprecated since version 2.5, to be removed in 3.0. - */ -interface ExceptionHandlerInterface -{ - /** - * Handles an exception. - * - * @param \Exception $exception An \Exception instance - */ - public function handle(\Exception $exception); -} diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php new file mode 100644 index 0000000000000..f46ef71208bde --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Configures the ExceptionHandler. + * + * @author Nicolas Grekas + */ +class DebugHandlersListener implements EventSubscriberInterface +{ + private $exceptionHandler; + + public function __construct($exceptionHandler) + { + if (is_callable($exceptionHandler)) { + $this->exceptionHandler = $exceptionHandler; + } + } + + public function configure() + { + if ($this->exceptionHandler) { + $mainHandler = set_exception_handler('var_dump'); + restore_exception_handler(); + if ($mainHandler instanceof ExceptionHandler) { + $mainHandler->setHandler($this->exceptionHandler); + } + $this->exceptionHandler = null; + } + } + + public static function getSubscribedEvents() + { + return array(KernelEvents::REQUEST => array('configure', 2048)); + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php b/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php deleted file mode 100644 index 0677682810ea8..0000000000000 --- a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\EventListener; - -use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\KernelEvents; - -/** - * Injects a fatal error exceptions handler into the ErrorHandler. - * - * @author Nicolas Grekas - */ -class FatalErrorExceptionsListener implements EventSubscriberInterface -{ - private $handler = null; - - public function __construct($handler) - { - if (is_callable($handler)) { - $this->handler = $handler; - } - } - - public function injectHandler() - { - if ($this->handler) { - ErrorHandler::setFatalErrorExceptionHandler($this->handler); - $this->handler = null; - } - } - - public static function getSubscribedEvents() - { - // Don't register early as e.g. the Router is generally required by the handler - return array(KernelEvents::REQUEST => array('injectHandler', 8)); - } -} diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index c3556972e97d1..68d89c94e9be3 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -25,7 +25,6 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Debug\Exception\FatalErrorException; /** * HttpKernel notifies events to convert a Request object to a Response one. @@ -87,11 +86,16 @@ public function terminate(Request $request, Response $response) } /** + * @throws \LogicException If the request stack is empty + * * @internal */ - public function handleFatalErrorException(FatalErrorException $exception) + public function terminateWithException(\Exception $exception) { - $request = $this->requestStack->getMasterRequest(); + if (!$request = $this->requestStack->getMasterRequest()) { + throw new \LogicException('Request stack is empty', 0, $exception); + } + $response = $this->handleException($exception, $request, self::MASTER_REQUEST); $response->sendHeaders(); From e3255bf5259a87760e4702c2b0faca5f4f8ec6a8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 20 May 2014 09:49:05 +0200 Subject: [PATCH 159/323] [Debug] better ouf of memory error handling --- src/Symfony/Component/Debug/ErrorHandler.php | 17 ++++++++----- .../Debug/Exception/FatalErrorException.php | 24 +++++++++++-------- .../Debug/Exception/OutOfMemoryException.php | 21 ++++++++++++++++ .../Component/Debug/ExceptionHandler.php | 9 +++++++ 4 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 src/Symfony/Component/Debug/Exception/OutOfMemoryException.php diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 9a6f6fa1c3699..850f7d9c55376 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Debug\Exception\ContextErrorException; use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\Exception\OutOfMemoryException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; @@ -313,12 +314,16 @@ private function handleFatalError($exceptionHandler, array $error) $level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type']; $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); - $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line'], 3); - - foreach ($this->getFatalErrorHandlers() as $handler) { - if ($e = $handler->handleError($error, $exception)) { - $exception = $e; - break; + if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { + $exception = new OutOfMemoryException($message, 0, $error['type'], $error['file'], $error['line'], 3, false); + } else { + $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line'], 3, true); + + foreach ($this->getFatalErrorHandlers() as $handler) { + if ($e = $handler->handleError($error, $exception)) { + $exception = $e; + break; + } } } diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 4e29495f302cb..d5b58468c9c6d 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -20,7 +20,7 @@ */ class FatalErrorException extends \ErrorException { - public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null) + public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null, $traceArgs = true) { parent::__construct($message, $code, $severity, $filename, $lineno); @@ -28,28 +28,32 @@ public function __construct($message, $code, $severity, $filename, $lineno, $tra if (function_exists('xdebug_get_function_stack')) { $trace = xdebug_get_function_stack(); if (0 < $traceOffset) { - $trace = array_slice($trace, 0, -$traceOffset); + array_splice($trace, -$traceOffset); } - $trace = array_reverse($trace); - foreach ($trace as $i => $frame) { + foreach ($trace as &$frame) { if (!isset($frame['type'])) { // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 if (isset($frame['class'])) { - $trace[$i]['type'] = '::'; + $frame['type'] = '::'; } } elseif ('dynamic' === $frame['type']) { - $trace[$i]['type'] = '->'; + $frame['type'] = '->'; } elseif ('static' === $frame['type']) { - $trace[$i]['type'] = '::'; + $frame['type'] = '::'; } // XDebug also has a different name for the parameters array - if (isset($frame['params']) && !isset($frame['args'])) { - $trace[$i]['args'] = $frame['params']; - unset($trace[$i]['params']); + if (!$traceArgs) { + unset($frame['params'], $frame['args']); + } elseif (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + unset($frame['params']); } } + + unset($frame); + $trace = array_reverse($trace); } else { $trace = array(); } diff --git a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php new file mode 100644 index 0000000000000..fec1979836450 --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.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\Debug\Exception; + +/** + * Out of memory exception. + * + * @author Nicolas Grekas + */ +class OutOfMemoryException extends FatalErrorException +{ +} diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index 4979899afcdfb..bfbd78313fb2f 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\Exception\OutOfMemoryException; if (!defined('ENT_SUBSTITUTE')) { define('ENT_SUBSTITUTE', 8); @@ -62,6 +63,8 @@ public static function register($debug = true) * Sets a user exception handler. * * @param callable $handler An handler that will be called on Exception + * + * @return callable|null The previous exception handler if any */ public function setHandler($handler) { @@ -88,6 +91,12 @@ public function setHandler($handler) */ public function handle(\Exception $exception) { + if ($exception instanceof OutOfMemoryException) { + $this->sendPhpResponse($exception); + + return; + } + // To be as fail-safe as possible, the exception is first handled // by our simple exception handler, then by the user exception handler. // The latter takes precedence and any output from the former is cancelled, From fef698e22f136ffddb5a382bb1666c2b85044d09 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 19 May 2014 17:05:32 +0200 Subject: [PATCH 160/323] [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations --- .../PropertyAccess/PropertyAccessor.php | 17 +++- .../Fixtures/NonTraversableArrayObject.php | 65 +++++++++++++ ...yObject.php => TraversableArrayObject.php} | 2 +- .../Tests/PropertyAccessorArrayAccessTest.php | 86 ++++++++++++++++++ .../Tests/PropertyAccessorArrayObjectTest.php | 2 +- .../Tests/PropertyAccessorArrayTest.php | 2 +- .../Tests/PropertyAccessorCollectionTest.php | 91 ++++--------------- ...yAccessorNonTraversableArrayObjectTest.php | 22 +++++ ...rtyAccessorTraversableArrayObjectTest.php} | 8 +- 9 files changed, 212 insertions(+), 83 deletions(-) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php rename src/Symfony/Component/PropertyAccess/Tests/Fixtures/{CustomArrayObject.php => TraversableArrayObject.php} (93%) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTest.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorNonTraversableArrayObjectTest.php rename src/Symfony/Component/PropertyAccess/Tests/{PropertyAccessorCustomArrayObjectTest.php => PropertyAccessorTraversableArrayObjectTest.php} (53%) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index f10755f0f63de..4eb4f5279e781 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -231,7 +231,22 @@ private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $pr // Create missing nested arrays on demand if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { if (!$ignoreInvalidIndices) { - throw new NoSuchIndexException(sprintf('Cannot read property "%s". Available properties are "%s"', $property, print_r(array_keys($objectOrArray), true))); + if (!is_array($objectOrArray)) { + if (!$objectOrArray instanceof \Traversable) { + throw new NoSuchIndexException(sprintf( + 'Cannot read property "%s".', + $property + )); + } + + $objectOrArray = iterator_to_array($objectOrArray); + } + + throw new NoSuchIndexException(sprintf( + 'Cannot read property "%s". Available properties are "%s"', + $property, + print_r(array_keys($objectOrArray), true) + )); } $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null; diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php new file mode 100644 index 0000000000000..fd00a730b831e --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * This class is a hand written simplified version of PHP native `ArrayObject` + * class, to show that it behaves differently than the PHP native implementation. + */ +class NonTraversableArrayObject implements \ArrayAccess, \Countable, \Serializable +{ + private $array; + + public function __construct(array $array = null) + { + $this->array = $array ?: array(); + } + + public function offsetExists($offset) + { + return array_key_exists($offset, $this->array); + } + + public function offsetGet($offset) + { + return $this->array[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->array[] = $value; + } else { + $this->array[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->array[$offset]); + } + + public function count() + { + return count($this->array); + } + + public function serialize() + { + return serialize($this->array); + } + + public function unserialize($serialized) + { + $this->array = (array) unserialize((string) $serialized); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php similarity index 93% rename from src/Symfony/Component/PropertyAccess/Tests/Fixtures/CustomArrayObject.php rename to src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php index cd23f7033fc6f..3bd9795e6b256 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php @@ -15,7 +15,7 @@ * This class is a hand written simplified version of PHP native `ArrayObject` * class, to show that it behaves differently than the PHP native implementation. */ -class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable +class TraversableArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable { private $array; diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTest.php new file mode 100644 index 0000000000000..a253d4030f1ef --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayAccessTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; + +abstract class PropertyAccessorArrayAccessTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PropertyAccessor + */ + protected $propertyAccessor; + + protected function setUp() + { + $this->propertyAccessor = new PropertyAccessor(); + } + + abstract protected function getContainer(array $array); + + public function getValidPropertyPaths() + { + return array( + array($this->getContainer(array('firstName' => 'Bernhard')), '[firstName]', 'Bernhard'), + array($this->getContainer(array('person' => $this->getContainer(array('firstName' => 'Bernhard')))), '[person][firstName]', 'Bernhard'), + ); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testGetValue($collection, $path, $value) + { + $this->assertSame($value, $this->propertyAccessor->getValue($collection, $path)); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchIndexException + */ + public function testGetValueFailsIfNoSuchIndex() + { + $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + + $object = $this->getContainer(array('firstName' => 'Bernhard')); + + $this->propertyAccessor->getValue($object, '[lastName]'); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testSetValue($collection, $path) + { + $this->propertyAccessor->setValue($collection, $path, 'Updated'); + + $this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path)); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsReadable($collection, $path) + { + $this->assertTrue($this->propertyAccessor->isReadable($collection, $path)); + } + + /** + * @dataProvider getValidPropertyPaths + */ + public function testIsWritable($collection, $path) + { + $this->assertTrue($this->propertyAccessor->isWritable($collection, $path, 'Updated')); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php index aaa86b3c25f8a..fb0b383789ba5 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php @@ -13,7 +13,7 @@ class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest { - protected function getCollection(array $array) + protected function getContainer(array $array) { return new \ArrayObject($array); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php index 5ab63c67cb32b..c982826344cec 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php @@ -13,7 +13,7 @@ class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest { - protected function getCollection(array $array) + protected function getContainer(array $array) { return $array; } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 60bd9dead870e..2312ac34fd576 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -11,8 +11,6 @@ namespace Symfony\Component\PropertyAccess\Tests; -use Symfony\Component\PropertyAccess\PropertyAccessor; - class PropertyAccessorCollectionTest_Car { private $axes; @@ -80,55 +78,13 @@ public function removeAxis($axis) {} public function getAxes() {} } -abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCase +abstract class PropertyAccessorCollectionTest extends PropertyAccessorArrayAccessTest { - /** - * @var PropertyAccessor - */ - private $propertyAccessor; - - protected function setUp() - { - $this->propertyAccessor = new PropertyAccessor(); - } - - abstract protected function getCollection(array $array); - - public function getValidPropertyPaths() - { - return array( - array(array('firstName' => 'Bernhard'), '[firstName]', 'Bernhard'), - array(array('person' => array('firstName' => 'Bernhard')), '[person][firstName]', 'Bernhard'), - ); - } - - /** - * @dataProvider getValidPropertyPaths - */ - public function testGetValue(array $array, $path, $value) - { - $collection = $this->getCollection($array); - - $this->assertSame($value, $this->propertyAccessor->getValue($collection, $path)); - } - - /** - * @dataProvider getValidPropertyPaths - */ - public function testSetValue(array $array, $path) - { - $collection = $this->getCollection($array); - - $this->propertyAccessor->setValue($collection, $path, 'Updated'); - - $this->assertSame('Updated', $this->propertyAccessor->getValue($collection, $path)); - } - public function testSetValueCallsAdderAndRemoverForCollections() { - $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth', 4 => 'fifth')); - $axesMerged = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); - $axesAfter = $this->getCollection(array(1 => 'second', 5 => 'first', 6 => 'third')); + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth', 4 => 'fifth')); + $axesMerged = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axesAfter = $this->getContainer(array(1 => 'second', 5 => 'first', 6 => 'third')); $axesMergedCopy = is_object($axesMerged) ? clone $axesMerged : $axesMerged; // Don't use a mock in order to test whether the collections are @@ -147,8 +103,8 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() { $car = $this->getMock(__CLASS__.'_CompositeCar'); $structure = $this->getMock(__CLASS__.'_CarStructure'); - $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); - $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth')); + $axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third')); $car->expects($this->any()) ->method('getStructure') @@ -177,35 +133,20 @@ public function testSetValueCallsAdderAndRemoverForNestedCollections() public function testSetValueFailsIfNoAdderNorRemoverFound() { $car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover'); - $axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); + $axesBefore = $this->getContainer(array(1 => 'second', 3 => 'fourth')); + $axesAfter = $this->getContainer(array(0 => 'first', 1 => 'second', 2 => 'third')); - $this->propertyAccessor->setValue($car, 'axes', $axes); - } - - /** - * @dataProvider getValidPropertyPaths - */ - public function testIsReadable(array $array, $path) - { - $collection = $this->getCollection($array); - - $this->assertTrue($this->propertyAccessor->isReadable($collection, $path)); - } - - /** - * @dataProvider getValidPropertyPaths - */ - public function testIsWritable(array $array, $path) - { - $collection = $this->getCollection($array); + $car->expects($this->any()) + ->method('getAxes') + ->will($this->returnValue($axesBefore)); - $this->assertTrue($this->propertyAccessor->isWritable($collection, $path, 'Updated')); + $this->propertyAccessor->setValue($car, 'axes', $axesAfter); } public function testIsWritableReturnsTrueIfAdderAndRemoverExists() { $car = $this->getMock(__CLASS__.'_Car'); - $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); $this->assertTrue($this->propertyAccessor->isWritable($car, 'axes', $axes)); } @@ -213,7 +154,7 @@ public function testIsWritableReturnsTrueIfAdderAndRemoverExists() public function testIsWritableReturnsFalseIfOnlyAdderExists() { $car = $this->getMock(__CLASS__.'_CarOnlyAdder'); - $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes)); } @@ -221,7 +162,7 @@ public function testIsWritableReturnsFalseIfOnlyAdderExists() public function testIsWritableReturnsFalseIfOnlyRemoverExists() { $car = $this->getMock(__CLASS__.'_CarOnlyRemover'); - $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes)); } @@ -229,7 +170,7 @@ public function testIsWritableReturnsFalseIfOnlyRemoverExists() public function testIsWritableReturnsFalseIfNoAdderNorRemoverExists() { $car = $this->getMock(__CLASS__.'_CarNoAdderAndRemover'); - $axes = $this->getCollection(array(1 => 'first', 2 => 'second', 3 => 'third')); + $axes = $this->getContainer(array(1 => 'first', 2 => 'second', 3 => 'third')); $this->assertFalse($this->propertyAccessor->isWritable($car, 'axes', $axes)); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorNonTraversableArrayObjectTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorNonTraversableArrayObjectTest.php new file mode 100644 index 0000000000000..6910d8be7031b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorNonTraversableArrayObjectTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\Tests\Fixtures\NonTraversableArrayObject; + +class PropertyAccessorNonTraversableArrayObjectTest extends PropertyAccessorArrayAccessTest +{ + protected function getContainer(array $array) + { + return new NonTraversableArrayObject($array); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTraversableArrayObjectTest.php similarity index 53% rename from src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php rename to src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTraversableArrayObjectTest.php index 7340df720fbf9..4e45001176d03 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTraversableArrayObjectTest.php @@ -11,12 +11,12 @@ namespace Symfony\Component\PropertyAccess\Tests; -use Symfony\Component\PropertyAccess\Tests\Fixtures\CustomArrayObject; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TraversableArrayObject; -class PropertyAccessorCustomArrayObjectTest extends PropertyAccessorCollectionTest +class PropertyAccessorTraversableArrayObjectTest extends PropertyAccessorCollectionTest { - protected function getCollection(array $array) + protected function getContainer(array $array) { - return new CustomArrayObject($array); + return new TraversableArrayObject($array); } } From bbe1045989402586bd6070997cee34b2a92189f3 Mon Sep 17 00:00:00 2001 From: Yannick Date: Wed, 9 Apr 2014 14:55:47 +0200 Subject: [PATCH 161/323] [Validator][Console][HttpFoundation] Use "KiB" everywhere (instead of "kB") --- .../Component/Console/Helper/Helper.php | 6 ++-- .../Console/Tests/Helper/ProgressBarTest.php | 6 ++-- .../HttpFoundation/File/UploadedFile.php | 2 +- .../Validator/Constraints/FileValidator.php | 8 ++--- .../Tests/Constraints/FileValidatorTest.php | 32 ++++++++++++++++--- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 05e895f765a1a..cc3205522e657 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -92,15 +92,15 @@ public static function formatTime($secs) public static function formatMemory($memory) { if ($memory >= 1024 * 1024 * 1024) { - return sprintf('%.1f GB', $memory / 1024 / 1024 / 1024); + return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); } if ($memory >= 1024 * 1024) { - return sprintf('%.1f MB', $memory / 1024 / 1024); + return sprintf('%.1f MiB', $memory / 1024 / 1024); } if ($memory >= 1024) { - return sprintf('%d kB', $memory / 1024); + return sprintf('%d KiB', $memory / 1024); } return sprintf('%d B', $memory); diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 7bcfed026b9a9..ba5db2921bd2c 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -367,17 +367,17 @@ public function testAnsiColorsAndEmojis() $this->generateOutput( " \033[44;37m Starting the demo... fingers crossed \033[0m\n". " 0/15 ".$progress.str_repeat($empty, 26)." 0%\n". - " 🏁 1 sec \033[44;37m 0 B \033[0m" + " \xf0\x9f\x8f\x81 1 sec \033[44;37m 0 B \033[0m" ). $this->generateOutput( " \033[44;37m Looks good to me... \033[0m\n". " 4/15 ".str_repeat($done, 7).$progress.str_repeat($empty, 19)." 26%\n". - " 🏁 1 sec \033[41;37m 97 kB \033[0m" + " \xf0\x9f\x8f\x81 1 sec \033[41;37m 97 KiB \033[0m" ). $this->generateOutput( " \033[44;37m Thanks, bye \033[0m\n". " 15/15 ".str_repeat($done, 28)." 100%\n". - " 🏁 1 sec \033[41;37m 195 kB \033[0m" + " \xf0\x9f\x8f\x81 1 sec \033[41;37m 195 KiB \033[0m" ), stream_get_contents($output->getStream()) ); diff --git a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php index 0bba18fb099c0..31f349c759a95 100644 --- a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php +++ b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -291,7 +291,7 @@ public static function getMaxFilesize() public function getErrorMessage() { static $errors = array( - UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d kb).', + UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', UPLOAD_ERR_NO_FILE => 'No file was uploaded.', diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php index b9d4fe1644362..736a9d0cddd3d 100644 --- a/src/Symfony/Component/Validator/Constraints/FileValidator.php +++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php @@ -45,9 +45,9 @@ public function validate($value, Constraint $constraint) if (ctype_digit((string) $constraint->maxSize)) { $maxSize = (int) $constraint->maxSize; } elseif (preg_match('/^\d++k$/', $constraint->maxSize)) { - $maxSize = $constraint->maxSize * 1024; + $maxSize = $constraint->maxSize * 1000; } elseif (preg_match('/^\d++M$/', $constraint->maxSize)) { - $maxSize = $constraint->maxSize * 1048576; + $maxSize = $constraint->maxSize * 1000 * 1000; } else { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize)); } @@ -119,9 +119,9 @@ public function validate($value, Constraint $constraint) } elseif (preg_match('/^\d++k$/', $constraint->maxSize)) { $size = round(filesize($path) / 1000, 2); $limit = (int) $constraint->maxSize; - $suffix = 'kB'; + $suffix = 'KB'; } elseif (preg_match('/^\d++M$/', $constraint->maxSize)) { - $size = round(filesize($path) / 1000000, 2); + $size = round(filesize($path) / (1000 * 1000), 2); $limit = (int) $constraint->maxSize; $suffix = 'MB'; } else { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index f5178cc029af7..f5c52c30dca3f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -116,8 +116,8 @@ public function testTooLargeKiloBytes() ->method('addViolation') ->with('myMessage', array( '{{ limit }}' => '1', - '{{ size }}' => '1.4', - '{{ suffix }}' => 'kB', + '{{ size }}' => '1.37', + '{{ suffix }}' => 'KiB', '{{ file }}' => $this->path, )); @@ -137,14 +137,38 @@ public function testTooLargeMegaBytes() ->method('addViolation') ->with('myMessage', array( '{{ limit }}' => '1', - '{{ size }}' => '1.4', - '{{ suffix }}' => 'MB', + '{{ size }}' => '1.34', + '{{ suffix }}' => 'MiB', '{{ file }}' => $this->path, )); $this->validator->validate($this->getFile($this->path), $constraint); } + public function testMaxSizeKiloBytes() + { + fwrite($this->file, str_repeat('0', 1010)); + + $constraint = new File(array( + 'maxSize' => '1k', + )); + + $this->context->expects($this->never())->method('addViolation'); + $this->validator->validate($this->getFile($this->path), $constraint); + } + + public function testMaxSizeMegaBytes() + { + fwrite($this->file, str_repeat('0', (1024 * 1022))); + + $constraint = new File(array( + 'maxSize' => '1M', + )); + + $this->context->expects($this->never())->method('addViolation'); + $this->validator->validate($this->getFile($this->path), $constraint); + } + /** * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException */ From e4c6da548b5ecb010ef806705c6d7114981c1190 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 20 May 2014 16:38:02 +0200 Subject: [PATCH 162/323] [Validator] Improved to-string conversion of the file size/size limit --- .../Validator/Constraints/FileValidator.php | 77 ++++++++---- .../Tests/Constraints/FileValidatorTest.php | 114 ++++++++++-------- 2 files changed, 119 insertions(+), 72 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php index 736a9d0cddd3d..22273c4dcb900 100644 --- a/src/Symfony/Component/Validator/Constraints/FileValidator.php +++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php @@ -25,6 +25,16 @@ */ class FileValidator extends ConstraintValidator { + const KB_BYTES = 1000; + + const MB_BYTES = 1000000; + + private static $suffices = array( + 1 => 'bytes', + self::KB_BYTES => 'kB', + self::MB_BYTES => 'MB', + ); + /** * {@inheritdoc} */ @@ -43,21 +53,21 @@ public function validate($value, Constraint $constraint) case UPLOAD_ERR_INI_SIZE: if ($constraint->maxSize) { if (ctype_digit((string) $constraint->maxSize)) { - $maxSize = (int) $constraint->maxSize; + $limitInBytes = (int) $constraint->maxSize; } elseif (preg_match('/^\d++k$/', $constraint->maxSize)) { - $maxSize = $constraint->maxSize * 1000; + $limitInBytes = $constraint->maxSize * self::KB_BYTES; } elseif (preg_match('/^\d++M$/', $constraint->maxSize)) { - $maxSize = $constraint->maxSize * 1000 * 1000; + $limitInBytes = $constraint->maxSize * self::MB_BYTES; } else { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize)); } - $maxSize = min(UploadedFile::getMaxFilesize(), $maxSize); + $limitInBytes = min(UploadedFile::getMaxFilesize(), $limitInBytes); } else { - $maxSize = UploadedFile::getMaxFilesize(); + $limitInBytes = UploadedFile::getMaxFilesize(); } $this->context->addViolation($constraint->uploadIniSizeErrorMessage, array( - '{{ limit }}' => $maxSize, + '{{ limit }}' => $limitInBytes, '{{ suffix }}' => 'bytes', )); @@ -112,27 +122,45 @@ public function validate($value, Constraint $constraint) } if ($constraint->maxSize) { - if (ctype_digit((string) $constraint->maxSize)) { - $size = filesize($path); - $limit = (int) $constraint->maxSize; - $suffix = 'bytes'; - } elseif (preg_match('/^\d++k$/', $constraint->maxSize)) { - $size = round(filesize($path) / 1000, 2); - $limit = (int) $constraint->maxSize; - $suffix = 'KB'; + $sizeInBytes = filesize($path); + $limitInBytes = (int) $constraint->maxSize; + + if (preg_match('/^\d++k$/', $constraint->maxSize)) { + $limitInBytes *= self::KB_BYTES; } elseif (preg_match('/^\d++M$/', $constraint->maxSize)) { - $size = round(filesize($path) / (1000 * 1000), 2); - $limit = (int) $constraint->maxSize; - $suffix = 'MB'; - } else { + $limitInBytes *= self::MB_BYTES; + } elseif (!ctype_digit((string) $constraint->maxSize)) { throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize)); } - if ($size > $limit) { + if ($sizeInBytes > $limitInBytes) { + // Convert the limit to the smallest possible number + // (i.e. try "MB", then "kB", then "bytes") + $coef = self::MB_BYTES; + $limitAsString = (string) ($limitInBytes / $coef); + + // Restrict the limit to 2 decimals (without rounding! we + // need the precise value) + while (self::moreDecimalsThan($limitAsString, 2)) { + $coef /= 1000; + $limitAsString = (string) ($limitInBytes / $coef); + } + + // Convert size to the same measure, but round to 2 decimals + $sizeAsString = (string) round($sizeInBytes / $coef, 2); + + // If the size and limit produce the same string output + // (due to rounding), reduce the coefficient + while ($sizeAsString === $limitAsString) { + $coef /= 1000; + $limitAsString = (string) ($limitInBytes / $coef); + $sizeAsString = (string) round($sizeInBytes / $coef, 2); + } + $this->context->addViolation($constraint->maxSizeMessage, array( - '{{ size }}' => $size, - '{{ limit }}' => $limit, - '{{ suffix }}' => $suffix, + '{{ size }}' => $sizeAsString, + '{{ limit }}' => $limitAsString, + '{{ suffix }}' => static::$suffices[$coef], '{{ file }}' => $path, )); @@ -172,4 +200,9 @@ public function validate($value, Constraint $constraint) } } } + + private static function moreDecimalsThan($double, $numberOfDecimals) + { + return strlen((string) $double) > strlen(round($double, $numberOfDecimals)); + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index f5c52c30dca3f..d2d2bcb34f257 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -33,7 +33,13 @@ protected function setUp() protected function tearDown() { - fclose($this->file); + if (is_resource($this->file)) { + fclose($this->file); + } + + if (file_exists($this->path)) { + unlink($this->path); + } $this->context = null; $this->validator = null; @@ -82,90 +88,98 @@ public function testValidUploadedfile() $this->validator->validate($file, new File()); } - public function testTooLargeBytes() + public function provideMaxSizeExceededTests() { - fwrite($this->file, str_repeat('0', 11)); + return array( + array(11, 10, '11', '10', 'bytes'), - $constraint = new File(array( - 'maxSize' => 10, - 'maxSizeMessage' => 'myMessage', - )); + array(ceil(1.005*1000), ceil(1.005*1000) - 1, '1005', '1004', 'bytes'), + array(ceil(1.005*1000*1000), ceil(1.005*1000*1000) - 1, '1005000', '1004999', 'bytes'), - $this->context->expects($this->once()) - ->method('addViolation') - ->with('myMessage', array( - '{{ limit }}' => '10', - '{{ size }}' => '11', - '{{ suffix }}' => 'bytes', - '{{ file }}' => $this->path, - )); + // round(size) == 1.01kB, limit == 1kB + array(ceil(1.005*1000), 1000, '1.01', '1', 'kB'), + array(ceil(1.005*1000), '1k', '1.01', '1', 'kB'), - $this->validator->validate($this->getFile($this->path), $constraint); - } + // round(size) == 1kB, limit == 1kB -> use bytes + array(ceil(1.004*1000), 1000, '1004', '1000', 'bytes'), + array(ceil(1.004*1000), '1k', '1004', '1000', 'bytes'), - public function testTooLargeKiloBytes() - { - fwrite($this->file, str_repeat('0', 1400)); + array(1000 + 1, 1000, '1001', '1000', 'bytes'), + array(1000 + 1, '1k', '1001', '1000', 'bytes'), - $constraint = new File(array( - 'maxSize' => '1k', - 'maxSizeMessage' => 'myMessage', - )); + // round(size) == 1.01MB, limit == 1MB + array(ceil(1.005*1000*1000), 1000*1000, '1.01', '1', 'MB'), + array(ceil(1.005*1000*1000), '1000k', '1.01', '1', 'MB'), + array(ceil(1.005*1000*1000), '1M', '1.01', '1', 'MB'), - $this->context->expects($this->once()) - ->method('addViolation') - ->with('myMessage', array( - '{{ limit }}' => '1', - '{{ size }}' => '1.37', - '{{ suffix }}' => 'KiB', - '{{ file }}' => $this->path, - )); + // round(size) == 1MB, limit == 1MB -> use kB + array(ceil(1.004*1000*1000), 1000*1000, '1004', '1000', 'kB'), + array(ceil(1.004*1000*1000), '1000k', '1004', '1000', 'kB'), + array(ceil(1.004*1000*1000), '1M', '1004', '1000', 'kB'), - $this->validator->validate($this->getFile($this->path), $constraint); + array(1000*1000 + 1, 1000*1000, '1000001', '1000000', 'bytes'), + array(1000*1000 + 1, '1000k', '1000001', '1000000', 'bytes'), + array(1000*1000 + 1, '1M', '1000001', '1000000', 'bytes'), + ); } - public function testTooLargeMegaBytes() + /** + * @dataProvider provideMaxSizeExceededTests + */ + public function testMaxSizeExceeded($bytesWritten, $limit, $sizeAsString, $limitAsString, $suffix) { - fwrite($this->file, str_repeat('0', 1400000)); + fseek($this->file, $bytesWritten-1, SEEK_SET); + fwrite($this->file, '0'); + fclose($this->file); $constraint = new File(array( - 'maxSize' => '1M', + 'maxSize' => $limit, 'maxSizeMessage' => 'myMessage', )); $this->context->expects($this->once()) ->method('addViolation') ->with('myMessage', array( - '{{ limit }}' => '1', - '{{ size }}' => '1.34', - '{{ suffix }}' => 'MiB', + '{{ limit }}' => $limitAsString, + '{{ size }}' => $sizeAsString, + '{{ suffix }}' => $suffix, '{{ file }}' => $this->path, )); $this->validator->validate($this->getFile($this->path), $constraint); } - public function testMaxSizeKiloBytes() + public function provideMaxSizeNotExceededTests() { - fwrite($this->file, str_repeat('0', 1010)); + return array( + array(10, 10), + array(9, 10), - $constraint = new File(array( - 'maxSize' => '1k', - )); + array(1000, '1k'), + array(1000 - 1, '1k'), - $this->context->expects($this->never())->method('addViolation'); - $this->validator->validate($this->getFile($this->path), $constraint); + array(1000*1000, '1M'), + array(1000*1000 - 1, '1M'), + ); } - public function testMaxSizeMegaBytes() + /** + * @dataProvider provideMaxSizeNotExceededTests + */ + public function testMaxSizeNotExceeded($bytesWritten, $limit) { - fwrite($this->file, str_repeat('0', (1024 * 1022))); + fseek($this->file, $bytesWritten-1, SEEK_SET); + fwrite($this->file, '0'); + fclose($this->file); $constraint = new File(array( - 'maxSize' => '1M', + 'maxSize' => $limit, + 'maxSizeMessage' => 'myMessage', )); - $this->context->expects($this->never())->method('addViolation'); + $this->context->expects($this->never()) + ->method('addViolation'); + $this->validator->validate($this->getFile($this->path), $constraint); } From 53b9d737f10b602157aea92cf42a0e2eefce83bc Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Sat, 17 May 2014 20:36:38 +0200 Subject: [PATCH 163/323] [Process] Deprecate Process::setStdin in favor of Process::setInput --- UPGRADE-3.0.md | 6 ++- src/Symfony/Component/Process/CHANGELOG.md | 2 + src/Symfony/Component/Process/Process.php | 50 +++++++++++++++---- .../Component/Process/ProcessBuilder.php | 10 ++-- .../Process/Tests/AbstractProcessTest.php | 23 +++++---- .../Process/Tests/SimpleProcessTest.php | 4 +- 6 files changed, 67 insertions(+), 28 deletions(-) diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 9c5c3dc475e71..baf3fce90449c 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -966,4 +966,8 @@ UPGRADE FROM 2.x to 3.0 ``` Yaml::parse(file_get_contents($fileName)); - ``` + +### Process + + * Process::setStdin() and Process::getStdin() have been removed. Use + Process::setInput() and Process::getInput() that works the same way. diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index fff694f3d0793..15211f2f251fe 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG * added support for PTY mode * added the convenience method "mustRun" + * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() + * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() 2.4.0 ----- diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 7ebbdbe43f05d..2879f6261eedb 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -45,7 +45,7 @@ class Process private $commandline; private $cwd; private $env; - private $stdin; + private $input; private $starttime; private $lastOutputTime; private $timeout; @@ -128,7 +128,7 @@ class Process * @param string $commandline The command line to run * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to inherit - * @param string|null $stdin The STDIN content + * @param string|null $input The input * @param int|float|null $timeout The timeout in seconds or null to disable * @param array $options An array of options for proc_open * @@ -136,7 +136,7 @@ class Process * * @api */ - public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) { if (!function_exists('proc_open')) { throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); @@ -156,7 +156,7 @@ public function __construct($commandline, $cwd = null, array $env = null, $stdin $this->setEnv($env); } - $this->stdin = $stdin; + $this->input = $input; $this->setTimeout($timeout); $this->useFileHandles = defined('PHP_WINDOWS_VERSION_BUILD'); $this->pty = false; @@ -224,7 +224,7 @@ public function mustRun($callback = null) } /** - * Starts the process and returns after sending the STDIN. + * Starts the process and returns after writing the input to STDIN. * * This method blocks until all STDIN data is sent to the process then it * returns while the process runs in the background. @@ -288,7 +288,7 @@ public function start($callback = null) return; } - $this->processPipes->write(false, $this->stdin); + $this->processPipes->write(false, $this->input); $this->updateStatus(false); $this->checkTimeout(); } @@ -1038,10 +1038,23 @@ public function setEnv(array $env) * Gets the contents of STDIN. * * @return string|null The current contents + * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * This method is deprecated in favor of getInput. */ public function getStdin() { - return $this->stdin; + return $this->getInput(); + } + + /** + * Gets the Process input. + * + * @return null|string The Process input + */ + public function getInput() + { + return $this->input; } /** @@ -1052,14 +1065,33 @@ public function getStdin() * @return self The current Process instance * * @throws LogicException In case the process is running + * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * This method is deprecated in favor of setInput. */ public function setStdin($stdin) + { + return $this->setInput($stdin); + } + + /** + * Sets the input. + * + * This content will be passed to the underlying process standard input. + * + * @param string|null $input The content + * + * @return self The current Process instance + * + * @throws LogicException In case the process is running + */ + public function setInput($input) { if ($this->isRunning()) { - throw new LogicException('STDIN can not be set while the process is running.'); + throw new LogicException('Input can not be set while the process is running.'); } - $this->stdin = $stdin; + $this->input = $input; return $this; } diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index 6e69df1387347..ae3f51d1e26ab 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -24,7 +24,7 @@ class ProcessBuilder private $arguments; private $cwd; private $env = array(); - private $stdin; + private $input; private $timeout = 60; private $options = array(); private $inheritEnv = true; @@ -156,13 +156,13 @@ public function addEnvironmentVariables(array $variables) /** * Sets the input of the process. * - * @param string $stdin The input as a string + * @param string $input The input as a string * * @return ProcessBuilder */ - public function setInput($stdin) + public function setInput($input) { - $this->stdin = $stdin; + $this->input = $input; return $this; } @@ -261,7 +261,7 @@ public function getProcess() $env = $this->env; } - $process = new Process($script, $this->cwd, $env, $this->stdin, $this->timeout, $options); + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); if ($this->outputDisabled) { $process->disableOutput(); diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index ec02407856bd3..4ea9f19f5d589 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -151,23 +151,23 @@ public function testProcessPipes($code, $size) $expectedLength = (1024 * $size) + 1; $p = $this->getProcess(sprintf('php -r %s', escapeshellarg($code))); - $p->setStdin($expected); + $p->setInput($expected); $p->run(); $this->assertEquals($expectedLength, strlen($p->getOutput())); $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); } - public function testSetStdinWhileRunningThrowsAnException() + public function testSetInputWhileRunningThrowsAnException() { $process = $this->getProcess('php -r "usleep(500000);"'); $process->start(); try { - $process->setStdin('foobar'); + $process->setInput('foobar'); $process->stop(); $this->fail('A LogicException should have been raised.'); } catch (LogicException $e) { - $this->assertEquals('STDIN can not be set while the process is running.', $e->getMessage()); + $this->assertEquals('Input can not be set while the process is running.', $e->getMessage()); } $process->stop(); } @@ -993,6 +993,7 @@ public function methodProvider() array('WorkingDirectory'), array('Env'), array('Stdin'), + array('Input'), array('Options') ); @@ -1000,14 +1001,14 @@ public function methodProvider() } /** - * @param string $commandline - * @param null $cwd - * @param array $env - * @param null $stdin - * @param int $timeout - * @param array $options + * @param string $commandline + * @param null|string $cwd + * @param null|array $env + * @param null|string $input + * @param int $timeout + * @param array $options * * @return Process */ - abstract protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()); + abstract protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()); } diff --git a/src/Symfony/Component/Process/Tests/SimpleProcessTest.php b/src/Symfony/Component/Process/Tests/SimpleProcessTest.php index 69ad3d5b09f1d..5c0f6d6279bbe 100644 --- a/src/Symfony/Component/Process/Tests/SimpleProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SimpleProcessTest.php @@ -150,9 +150,9 @@ public function testSignalWithWrongNonIntSignal() /** * {@inheritdoc} */ - protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) { - return new Process($commandline, $cwd, $env, $stdin, $timeout, $options); + return new Process($commandline, $cwd, $env, $input, $timeout, $options); } private function skipIfPHPSigchild() From ad348a9b07e298142dc1715ba1b7862888e0337f Mon Sep 17 00:00:00 2001 From: Tobias Schultze Date: Thu, 15 May 2014 19:19:48 +0200 Subject: [PATCH 164/323] [HttpFoundation] implement session locking for PDO --- .../stubs/SessionHandlerInterface.php | 12 + .../Storage/Handler/PdoSessionHandler.php | 306 ++++++++++++++---- .../Storage/Handler/PdoSessionHandlerTest.php | 124 ++++--- 3 files changed, 327 insertions(+), 115 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php index 24280e38fca4a..9557135bcfbd1 100644 --- a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php +++ b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php @@ -12,6 +12,14 @@ /** * SessionHandlerInterface for PHP < 5.4 * + * The order in which these methods are invoked by PHP are: + * 1. open [session_start] + * 2. read + * 3. gc [optional depending on probability settings: gc_probability / gc_divisor] + * 4. destroy [optional when session_regenerate_id(true) is used] + * 5. write [session_write_close] or destroy [session_destroy] + * 6. close + * * Extensive documentation can be found at php.net, see links: * * @see http://php.net/sessionhandlerinterface @@ -19,6 +27,7 @@ * @see http://php.net/session-set-save-handler * * @author Drak + * @author Tobias Schultze */ interface SessionHandlerInterface { @@ -57,6 +66,9 @@ public function read($sessionId); /** * Writes the session data to the storage. * + * Care, the session ID passed to write() can be different from the one previously + * received in read() when the session ID changed due to session_regenerate_id(). + * * @see http://php.net/sessionhandlerinterface.write * * @param string $sessionId Session ID , see http://php.net/function.session-id diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index b831383a24d0b..4cdf3a89856e8 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -12,7 +12,19 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * PdoSessionHandler. + * Session handler using a PDO connection to read and write data. + * + * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements + * locking of sessions to prevent loss of data by concurrent access to the same session. + * This means requests for the same session will wait until the other one finished. + * PHPs internal files session handler also works this way. + * + * Attention: Since SQLite does not support row level locks but locks the whole database, + * it means only one session can be accessed at a time. Even different sessions would wait + * for another to finish. So saving session in SQLite should only be considered for + * development or prototypes. + * + * @see http://php.net/sessionhandlerinterface * * @author Fabien Potencier * @author Michael Williams @@ -25,6 +37,11 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $pdo; + /** + * @var string Database driver + */ + private $driver; + /** * @var string Table name */ @@ -45,39 +62,50 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $timeCol; + /** + * @var bool Whether a transaction is active + */ + private $inTransaction = false; + + /** + * @var bool Whether gc() has been called + */ + private $gcCalled = false; + /** * Constructor. * * List of available options: - * * db_table: The name of the table [required] + * * db_table: The name of the table [default: sessions] * * db_id_col: The column where to store the session id [default: sess_id] * * db_data_col: The column where to store the session data [default: sess_data] * * db_time_col: The column where to store the timestamp [default: sess_time] * - * @param \PDO $pdo A \PDO instance - * @param array $dbOptions An associative array of DB options + * @param \PDO $pdo A \PDO instance + * @param array $options An associative array of DB options * - * @throws \InvalidArgumentException When "db_table" option is not provided + * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION */ - public function __construct(\PDO $pdo, array $dbOptions = array()) + public function __construct(\PDO $pdo, array $options = array()) { - if (!array_key_exists('db_table', $dbOptions)) { - throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.'); - } if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) { throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); } + $this->pdo = $pdo; - $dbOptions = array_merge(array( + $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + + $options = array_replace(array( + 'db_table' => 'sessions', 'db_id_col' => 'sess_id', 'db_data_col' => 'sess_data', 'db_time_col' => 'sess_time', - ), $dbOptions); + ), $options); - $this->table = $dbOptions['db_table']; - $this->idCol = $dbOptions['db_id_col']; - $this->dataCol = $dbOptions['db_data_col']; - $this->timeCol = $dbOptions['db_time_col']; + $this->table = $options['db_table']; + $this->idCol = $options['db_id_col']; + $this->dataCol = $options['db_data_col']; + $this->timeCol = $options['db_time_col']; } /** @@ -85,34 +113,45 @@ public function __construct(\PDO $pdo, array $dbOptions = array()) */ public function open($savePath, $sessionName) { - return true; - } + $this->gcCalled = false; - /** - * {@inheritdoc} - */ - public function close() - { return true; } /** * {@inheritdoc} */ - public function destroy($sessionId) + public function read($sessionId) { - // delete the record associated with this id - $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; + $this->beginTransaction(); try { + $this->lockSession($sessionId); + + // We need to make sure we do not return session data that is already considered garbage according + // to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. + $maxlifetime = (int) ini_get('session.gc_maxlifetime'); + + $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id AND $this->timeCol > :time"; + $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); $stmt->execute(); + + // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed + $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); + + if ($sessionRows) { + return base64_decode($sessionRows[0][0]); + } + + return ''; } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e); - } + $this->rollback(); - return true; + throw $e; + } } /** @@ -120,16 +159,9 @@ public function destroy($sessionId) */ public function gc($maxlifetime) { - // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->timeCol < :time"; - - try { - $stmt = $this->pdo->prepare($sql); - $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); - $stmt->execute(); - } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e); - } + // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. + // This way, pruning expired sessions does not block them from being started while the current session is used. + $this->gcCalled = true; return true; } @@ -137,26 +169,22 @@ public function gc($maxlifetime) /** * {@inheritdoc} */ - public function read($sessionId) + public function destroy($sessionId) { - $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id"; + // delete the record associated with this id + $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; try { $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->execute(); - - // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed - $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); - - if ($sessionRows) { - return base64_decode($sessionRows[0][0]); - } - - return ''; } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e); + $this->rollback(); + + throw $e; } + + return true; } /** @@ -167,8 +195,10 @@ public function write($sessionId, $data) // Session data can contain non binary safe characters so we need to encode it. $encoded = base64_encode($data); + // The session ID can be different from the one previously received in read() + // when the session ID changed due to session_regenerate_id(). So we have to + // do an insert or update even if we created a row in read() for locking. // We use a MERGE SQL query when supported by the database. - // Otherwise we have to use a transactional DELETE followed by INSERT to prevent duplicate entries under high concurrency. try { $mergeSql = $this->getMergeSql(); @@ -183,15 +213,18 @@ public function write($sessionId, $data) return true; } - $this->pdo->beginTransaction(); - - try { - $deleteStmt = $this->pdo->prepare( - "DELETE FROM $this->table WHERE $this->idCol = :id" - ); - $deleteStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $deleteStmt->execute(); - + $updateStmt = $this->pdo->prepare( + "UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id" + ); + $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); + $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->execute(); + + // Since we have a lock on the session, this is safe to do. Otherwise it would be prone to + // race conditions in high concurrency. And if it's a regenerated session ID it should be + // unique anyway. + if (!$updateStmt->rowCount()) { $insertStmt = $this->pdo->prepare( "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)" ); @@ -199,18 +232,153 @@ public function write($sessionId, $data) $insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); $insertStmt->execute(); + } + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function close() + { + $this->commit(); + + if ($this->gcCalled) { + $maxlifetime = (int) ini_get('session.gc_maxlifetime'); + + // delete the session records that have expired + $sql = "DELETE FROM $this->table WHERE $this->timeCol <= :time"; - $this->pdo->commit(); + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); + $stmt->execute(); + } + + return true; + } + + /** + * Helper method to begin a transaction. + * + * Since SQLite does not support row level locks, we have to acquire a reserved lock + * on the database immediately. Because of https://bugs.php.net/42766 we have to create + * such a transaction manually which also means we cannot use PDO::commit or + * PDO::rollback or PDO::inTransaction for SQLite. + */ + private function beginTransaction() + { + if ($this->inTransaction) { + $this->rollback(); + + throw new \BadMethodCallException( + 'Session handler methods have been invoked in wrong sequence. ' . + 'Expected sequence: open() -> read() -> destroy() / write() -> close()'); + } + + if ('sqlite' === $this->driver) { + $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); + } else { + $this->pdo->beginTransaction(); + } + $this->inTransaction = true; + } + + /** + * Helper method to commit a transaction. + */ + private function commit() + { + if ($this->inTransaction) { + try { + // commit read-write transaction which also releases the lock + if ('sqlite' === $this->driver) { + $this->pdo->exec('COMMIT'); + } else { + $this->pdo->commit(); + } + $this->inTransaction = false; } catch (\PDOException $e) { - $this->pdo->rollback(); + $this->rollback(); throw $e; } - } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e); } + } - return true; + /** + * Helper method to rollback a transaction. + */ + private function rollback() + { + // We only need to rollback if we are in a transaction. Otherwise the resulting + // error would hide the real problem why rollback was called. We might not be + // in a transaction when two callbacks (e.g. destroy and write) are invoked that + // both fail. + if ($this->inTransaction) { + if ('sqlite' === $this->driver) { + $this->pdo->exec('ROLLBACK'); + } else { + $this->pdo->rollback(); + } + $this->inTransaction = false; + } + } + + /** + * Exclusively locks the row so other concurrent requests on the same session will block. + * + * This prevents loss of data by keeping the data consistent between read() and write(). + * We do not use SELECT FOR UPDATE because it does not lock non-existent rows. And a following + * INSERT when not found can result in a deadlock for one connection. + * + * @param string $sessionId Session ID + */ + private function lockSession($sessionId) + { + switch ($this->driver) { + case 'mysql': + // will also lock the row when actually nothing got updated (id = id) + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . + "ON DUPLICATE KEY UPDATE $this->idCol = $this->idCol"; + break; + case 'oci': + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . + "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol"; + break; + case 'sqlsrv': + // MS SQL Server requires MERGE be terminated by semicolon + $sql = "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . + "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol;"; + break; + case 'pgsql': + // obtain an exclusive transaction level advisory lock + $sql = 'SELECT pg_advisory_xact_lock(:lock_id)'; + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':lock_id', hexdec(substr($sessionId, 0, 15)), \PDO::PARAM_INT); + $stmt->execute(); + + return; + default: + return; + } + + // We create a DML lock for the session by inserting empty data or updating the row. + // This is safer than an application level advisory lock because it also prevents concurrent modification + // of the session from other parts of the application. + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindValue(':data', '', \PDO::PARAM_STR); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); } /** @@ -220,9 +388,7 @@ public function write($sessionId, $data) */ private function getMergeSql() { - $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); - - switch ($driver) { + switch ($this->driver) { case 'mysql': return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)"; @@ -230,12 +396,12 @@ private function getMergeSql() // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time"; case 'sqlsrv': // MS SQL Server requires MERGE be terminated by semicolon return "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data;"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;"; case 'sqlite': return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index e465f398984da..109addbcbd93a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -29,74 +29,108 @@ protected function setUp() $this->pdo->exec($sql); } - public function testIncompleteOptions() - { - $this->setExpectedException('InvalidArgumentException'); - $storage = new PdoSessionHandler($this->pdo, array()); - } - + /** + * @expectedException \InvalidArgumentException + */ public function testWrongPdoErrMode() { - $pdo = new \PDO("sqlite::memory:"); - $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); - $pdo->exec("CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)"); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); - $this->setExpectedException('InvalidArgumentException'); - $storage = new PdoSessionHandler($pdo, array('db_table' => 'sessions')); + $storage = new PdoSessionHandler($this->pdo); } - public function testWrongTableOptionsWrite() + /** + * @expectedException \RuntimeException + */ + public function testInexistentTable() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); - $this->setExpectedException('RuntimeException'); - $storage->write('foo', 'bar'); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'inexistent_table')); + $storage->open('', 'sid'); + $storage->read('id'); + $storage->write('id', 'data'); + $storage->close(); } - public function testWrongTableOptionsRead() + public function testReadWriteRead() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); - $this->setExpectedException('RuntimeException'); - $storage->read('foo', 'bar'); + $storage = new PdoSessionHandler($this->pdo); + $storage->open('', 'sid'); + $this->assertSame('', $storage->read('id'), 'New session returns empty string data'); + $storage->write('id', 'data'); + $storage->close(); + + $storage->open('', 'sid'); + $this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly'); + $storage->close(); } - public function testWriteRead() + /** + * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace) + */ + public function testWriteDifferentSessionIdThanRead() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage->write('foo', 'bar'); - $this->assertEquals('bar', $storage->read('foo'), 'written value can be read back correctly'); + $storage = new PdoSessionHandler($this->pdo); + $storage->open('', 'sid'); + $storage->read('id'); + $storage->destroy('id'); + $storage->write('new_id', 'data_of_new_session_id'); + $storage->close(); + + $storage->open('', 'sid'); + $this->assertSame('data_of_new_session_id', $storage->read('new_id'), 'Data of regenerated session id is available'); + $storage->close(); } - public function testMultipleInstances() + /** + * @expectedException \BadMethodCallException + */ + public function testWrongUsage() { - $storage1 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage1->write('foo', 'bar'); - - $storage2 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $this->assertEquals('bar', $storage2->read('foo'), 'values persist between instances'); + $storage = new PdoSessionHandler($this->pdo); + $storage->open('', 'sid'); + $storage->read('id'); + $storage->read('id'); } public function testSessionDestroy() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage->write('foo', 'bar'); - $this->assertCount(1, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); - - $storage->destroy('foo'); - - $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); + $storage = new PdoSessionHandler($this->pdo); + + $storage->open('', 'sid'); + $storage->read('id'); + $storage->write('id', 'data'); + $storage->close(); + $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + $storage->open('', 'sid'); + $storage->read('id'); + $storage->destroy('id'); + $storage->close(); + $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + $storage->open('', 'sid'); + $this->assertSame('', $storage->read('id'), 'Destroyed session returns empty string'); + $storage->close(); } public function testSessionGC() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - - $storage->write('foo', 'bar'); - $storage->write('baz', 'bar'); - - $this->assertCount(2, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); - - $storage->gc(-1); - $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); + $previousLifeTime = ini_set('session.gc_maxlifetime', 0); + $storage = new PdoSessionHandler($this->pdo); + + $storage->open('', 'sid'); + $storage->read('id'); + $storage->write('id', 'data'); + $storage->close(); + $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + $storage->open('', 'sid'); + $this->assertSame('', $storage->read('id'), 'Session already considered garbage, so not returning data even if it is not pruned yet'); + $storage->gc(0); + $storage->close(); + $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + ini_set('session.gc_maxlifetime', $previousLifeTime); } public function testGetConnection() From 3454d6084f9e536411011a345adae6718b661d4c Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Thu, 22 May 2014 22:31:58 +0200 Subject: [PATCH 165/323] [Process] Fix conflicts between latest 2.3 fix and 2.5 deprecation --- .../Process/Tests/AbstractProcessTest.php | 20 +++++++++---------- .../Tests/SigchildDisabledProcessTest.php | 4 ++-- .../Tests/SigchildEnabledProcessTest.php | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index e099faa268317..4e0f4b5bb1cb4 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -173,17 +173,17 @@ public function testSetInputWhileRunningThrowsAnException() } /** - * @dataProvider provideInvalidStdinValues + * @dataProvider provideInvalidInputValues * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException - * @expectedExceptionMessage Symfony\Component\Process\Process::setStdin only accepts strings. + * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings. */ - public function testInvalidStdin($value) + public function testInvalidInput($value) { $process = $this->getProcess('php -v'); - $process->setStdin($value); + $process->setInput($value); } - public function provideInvalidStdinValues() + public function provideInvalidInputValues() { return array( array(array()), @@ -193,16 +193,16 @@ public function provideInvalidStdinValues() } /** - * @dataProvider provideStdinValues + * @dataProvider provideInputValues */ - public function testValidStdin($expected, $value) + public function testValidInput($expected, $value) { $process = $this->getProcess('php -v'); - $process->setStdin($value); - $this->assertSame($expected, $process->getStdin()); + $process->setInput($value); + $this->assertSame($expected, $process->getInput()); } - public function provideStdinValues() + public function provideInputValues() { return array( array(null, null), diff --git a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php index 252098ec4c6d4..d0a0f25d1856c 100644 --- a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php @@ -230,9 +230,9 @@ public function testExitCodeIsAvailableAfterSignal() /** * {@inheritdoc} */ - protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) { - $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $stdin, $timeout, $options); + $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $input, $timeout, $options); $process->setEnhanceSigchildCompatibility(false); return $process; diff --git a/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php index 65dd4bb57364e..05b9dad6af9d3 100644 --- a/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php @@ -123,9 +123,9 @@ public function testStartAfterATimeout() /** * {@inheritdoc} */ - protected function getProcess($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) { - $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $stdin, $timeout, $options); + $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $input, $timeout, $options); $process->setEnhanceSigchildCompatibility(true); return $process; From 9887b831d25e1667456ed64acd04d1309c350083 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Sat, 17 May 2014 19:36:35 +0200 Subject: [PATCH 166/323] [Process] Deprecate using values that are not string for Process::setStdin and ProcessBuilder::setInput --- UPGRADE-3.0.md | 1 + src/Symfony/Component/Process/CHANGELOG.md | 1 + src/Symfony/Component/Process/Process.php | 2 ++ src/Symfony/Component/Process/ProcessBuilder.php | 2 ++ src/Symfony/Component/Process/ProcessUtils.php | 8 ++++++-- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index baf3fce90449c..70f56da36f010 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -971,3 +971,4 @@ UPGRADE FROM 2.x to 3.0 * Process::setStdin() and Process::getStdin() have been removed. Use Process::setInput() and Process::getInput() that works the same way. + * Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types. diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index 15211f2f251fe..2f3c1beb74b7e 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added the convenience method "mustRun" * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() + * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types 2.4.0 ----- diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index d0cf767dfaf68..e5edf59d96020 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -1060,6 +1060,8 @@ public function getInput() /** * Sets the contents of STDIN. * + * Deprecation: As of Symfony 2.5, this method only accepts scalar values. + * * @param string|null $stdin The new contents * * @return self The current Process instance diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index c5998681338e5..40b8d7011a336 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -156,6 +156,8 @@ public function addEnvironmentVariables(array $variables) /** * Sets the input of the process. * + * Deprecation: As of Symfony 2.5, this method only accepts string values. + * * @param string|null $input The input as a string * * @return ProcessBuilder diff --git a/src/Symfony/Component/Process/ProcessUtils.php b/src/Symfony/Component/Process/ProcessUtils.php index 441522d388048..35ae17c508822 100644 --- a/src/Symfony/Component/Process/ProcessUtils.php +++ b/src/Symfony/Component/Process/ProcessUtils.php @@ -75,7 +75,7 @@ public static function escapeArgument($argument) } /** - * Validates and normalized a Process input + * Validates and normalizes a Process input * * @param string $caller The name of method call that validates the input * @param mixed $input The input to validate @@ -87,7 +87,11 @@ public static function escapeArgument($argument) public static function validateInput($caller, $input) { if (null !== $input) { - if (is_scalar($input) || (is_object($input) && method_exists($input, '__toString'))) { + if (is_scalar($input)) { + return (string) $input; + } + // deprecated as of Symfony 2.5, to be removed in 3.0 + if (is_object($input) && method_exists($input, '__toString')) { return (string) $input; } From a11645c63740c29c10f49af961c9856676778349 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 23 May 2014 16:36:49 +0200 Subject: [PATCH 167/323] updated version to 2.6 --- composer.json | 2 +- src/Symfony/Bridge/Doctrine/composer.json | 2 +- src/Symfony/Bridge/Monolog/composer.json | 2 +- src/Symfony/Bridge/Propel1/composer.json | 2 +- src/Symfony/Bridge/ProxyManager/composer.json | 2 +- src/Symfony/Bridge/Swiftmailer/composer.json | 2 +- src/Symfony/Bridge/Twig/composer.json | 2 +- src/Symfony/Bundle/FrameworkBundle/composer.json | 2 +- src/Symfony/Bundle/SecurityBundle/composer.json | 2 +- src/Symfony/Bundle/TwigBundle/composer.json | 2 +- src/Symfony/Bundle/WebProfilerBundle/composer.json | 2 +- src/Symfony/Component/BrowserKit/composer.json | 2 +- src/Symfony/Component/ClassLoader/composer.json | 2 +- src/Symfony/Component/Config/composer.json | 2 +- src/Symfony/Component/Console/composer.json | 2 +- src/Symfony/Component/CssSelector/composer.json | 2 +- src/Symfony/Component/Debug/composer.json | 2 +- src/Symfony/Component/DependencyInjection/composer.json | 2 +- src/Symfony/Component/DomCrawler/composer.json | 2 +- src/Symfony/Component/EventDispatcher/composer.json | 2 +- src/Symfony/Component/ExpressionLanguage/composer.json | 2 +- src/Symfony/Component/Filesystem/composer.json | 2 +- src/Symfony/Component/Finder/composer.json | 2 +- src/Symfony/Component/Form/README.md | 2 +- src/Symfony/Component/Form/composer.json | 2 +- src/Symfony/Component/HttpFoundation/composer.json | 2 +- src/Symfony/Component/HttpKernel/Kernel.php | 6 +++--- src/Symfony/Component/HttpKernel/composer.json | 2 +- src/Symfony/Component/Intl/README.md | 2 +- src/Symfony/Component/Intl/composer.json | 2 +- src/Symfony/Component/Locale/composer.json | 2 +- src/Symfony/Component/OptionsResolver/composer.json | 2 +- src/Symfony/Component/Process/composer.json | 2 +- src/Symfony/Component/PropertyAccess/composer.json | 2 +- src/Symfony/Component/Routing/composer.json | 2 +- src/Symfony/Component/Security/Acl/README.md | 2 +- src/Symfony/Component/Security/Acl/composer.json | 2 +- src/Symfony/Component/Security/Core/README.md | 2 +- src/Symfony/Component/Security/Core/composer.json | 2 +- src/Symfony/Component/Security/Csrf/README.md | 2 +- src/Symfony/Component/Security/Csrf/composer.json | 2 +- src/Symfony/Component/Security/Http/README.md | 2 +- src/Symfony/Component/Security/Http/composer.json | 2 +- src/Symfony/Component/Security/README.md | 2 +- src/Symfony/Component/Security/composer.json | 2 +- src/Symfony/Component/Serializer/composer.json | 2 +- src/Symfony/Component/Stopwatch/composer.json | 2 +- src/Symfony/Component/Templating/composer.json | 2 +- src/Symfony/Component/Translation/README.md | 2 +- src/Symfony/Component/Translation/composer.json | 2 +- src/Symfony/Component/Validator/README.md | 2 +- src/Symfony/Component/Validator/composer.json | 2 +- src/Symfony/Component/Yaml/composer.json | 2 +- 53 files changed, 55 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index 7efd53bdfff40..aa44ad6a3a198 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 383df1ed370b1..16a0cc27c4628 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -45,7 +45,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 067f1c70d71d7..2ce523e5fe2b2 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -36,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bridge/Propel1/composer.json b/src/Symfony/Bridge/Propel1/composer.json index 6aa820d6b34e4..e1fe6f469a832 100644 --- a/src/Symfony/Bridge/Propel1/composer.json +++ b/src/Symfony/Bridge/Propel1/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 04cc581ac3817..9d9859b162a05 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bridge/Swiftmailer/composer.json b/src/Symfony/Bridge/Swiftmailer/composer.json index 8e5277692e706..d077c543b7e5a 100644 --- a/src/Symfony/Bridge/Swiftmailer/composer.json +++ b/src/Symfony/Bridge/Swiftmailer/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 44541136600bc..59db37b44f68d 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -50,7 +50,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 894f1f7869c32..f30def2e076b5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -52,7 +52,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index c54213c9bfb39..a8deb483a311a 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 49e32a193e098..3e6c430d02698 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index c6e5ee51248a9..15a9beffb85ca 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json index ed914dde46684..7ff2cab391ec2 100644 --- a/src/Symfony/Component/BrowserKit/composer.json +++ b/src/Symfony/Component/BrowserKit/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/ClassLoader/composer.json b/src/Symfony/Component/ClassLoader/composer.json index 84ce6a0a633a7..f323a76c6f5a7 100644 --- a/src/Symfony/Component/ClassLoader/composer.json +++ b/src/Symfony/Component/ClassLoader/composer.json @@ -28,7 +28,7 @@ "target-dir": "Symfony/Component/ClassLoader", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json index dce426b77c0e3..c11d3a603d601 100644 --- a/src/Symfony/Component/Config/composer.json +++ b/src/Symfony/Component/Config/composer.json @@ -26,7 +26,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index e7a8e2fc70ed8..40131af463357 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/CssSelector/composer.json b/src/Symfony/Component/CssSelector/composer.json index 5b4231d794757..d47c77fe6444a 100644 --- a/src/Symfony/Component/CssSelector/composer.json +++ b/src/Symfony/Component/CssSelector/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Debug/composer.json b/src/Symfony/Component/Debug/composer.json index b9cd2d340a363..447ecc8e9b42b 100644 --- a/src/Symfony/Component/Debug/composer.json +++ b/src/Symfony/Component/Debug/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 2190101fa2c9f..c0090495a1bff 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/DomCrawler/composer.json b/src/Symfony/Component/DomCrawler/composer.json index 0246346238c0f..dd305f7d1f112 100644 --- a/src/Symfony/Component/DomCrawler/composer.json +++ b/src/Symfony/Component/DomCrawler/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 3715ece302fb1..f07e3be4f22b7 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json index 3828f0435aa76..ebd1ba1b2e793 100644 --- a/src/Symfony/Component/ExpressionLanguage/composer.json +++ b/src/Symfony/Component/ExpressionLanguage/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Filesystem/composer.json b/src/Symfony/Component/Filesystem/composer.json index dfa633c1d06ce..981423e6ea1ce 100644 --- a/src/Symfony/Component/Filesystem/composer.json +++ b/src/Symfony/Component/Filesystem/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Finder/composer.json b/src/Symfony/Component/Finder/composer.json index b6e6997884e0f..a91b4707f7c02 100644 --- a/src/Symfony/Component/Finder/composer.json +++ b/src/Symfony/Component/Finder/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Form/README.md b/src/Symfony/Component/Form/README.md index 340bc6e63137f..ff353701f22f2 100644 --- a/src/Symfony/Component/Form/README.md +++ b/src/Symfony/Component/Form/README.md @@ -14,7 +14,7 @@ https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/FormServiceProvid Documentation: -http://symfony.com/doc/2.5/book/forms.html +http://symfony.com/doc/2.6/book/forms.html Resources --------- diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 56176c234ffd2..14412e1f61e05 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -42,7 +42,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index db57eb563ebfc..25294adb79445 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 99a3491e55bc7..293b474f07b5f 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,10 +60,10 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-DEV'; - const VERSION_ID = '20500'; + const VERSION = '2.6.0-DEV'; + const VERSION_ID = '20600'; const MAJOR_VERSION = '2'; - const MINOR_VERSION = '5'; + const MINOR_VERSION = '6'; const RELEASE_VERSION = '0'; const EXTRA_VERSION = 'DEV'; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 5ddee871a37af..7105b5cb12817 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -51,7 +51,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Intl/README.md b/src/Symfony/Component/Intl/README.md index f5a4ca0432440..60b2e3e42b290 100644 --- a/src/Symfony/Component/Intl/README.md +++ b/src/Symfony/Component/Intl/README.md @@ -22,4 +22,4 @@ You can run the unit tests with the following command: $ phpunit [0]: http://www.php.net/manual/en/intl.setup.php -[1]: http://symfony.com/doc/2.5/components/intl.html +[1]: http://symfony.com/doc/2.6/components/intl.html diff --git a/src/Symfony/Component/Intl/composer.json b/src/Symfony/Component/Intl/composer.json index 9be866e05fcb3..9b7148166e5bf 100644 --- a/src/Symfony/Component/Intl/composer.json +++ b/src/Symfony/Component/Intl/composer.json @@ -42,7 +42,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Locale/composer.json b/src/Symfony/Component/Locale/composer.json index 26015cea6ef89..a1dcfc0523f10 100644 --- a/src/Symfony/Component/Locale/composer.json +++ b/src/Symfony/Component/Locale/composer.json @@ -26,7 +26,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/OptionsResolver/composer.json b/src/Symfony/Component/OptionsResolver/composer.json index b0e71ea2997a1..f467e09b7fb51 100644 --- a/src/Symfony/Component/OptionsResolver/composer.json +++ b/src/Symfony/Component/OptionsResolver/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Process/composer.json b/src/Symfony/Component/Process/composer.json index b5dbfe1390a2b..39fa97cb59b05 100644 --- a/src/Symfony/Component/Process/composer.json +++ b/src/Symfony/Component/Process/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index a34297fd9ed11..ebf35abc283e2 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 9a88cc149fb33..1226298cc3047 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -38,7 +38,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Security/Acl/README.md b/src/Symfony/Component/Security/Acl/README.md index 6c009a368eb66..bb2d4d77f328a 100644 --- a/src/Symfony/Component/Security/Acl/README.md +++ b/src/Symfony/Component/Security/Acl/README.md @@ -11,7 +11,7 @@ Resources Documentation: -http://symfony.com/doc/2.5/book/security.html +http://symfony.com/doc/2.6/book/security.html Tests ----- diff --git a/src/Symfony/Component/Security/Acl/composer.json b/src/Symfony/Component/Security/Acl/composer.json index 5f5787fcc69f7..fb25b5d783a36 100644 --- a/src/Symfony/Component/Security/Acl/composer.json +++ b/src/Symfony/Component/Security/Acl/composer.json @@ -36,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Security/Core/README.md b/src/Symfony/Component/Security/Core/README.md index 4585a5d675332..66c323e65a75f 100644 --- a/src/Symfony/Component/Security/Core/README.md +++ b/src/Symfony/Component/Security/Core/README.md @@ -11,7 +11,7 @@ Resources Documentation: -http://symfony.com/doc/2.5/book/security.html +http://symfony.com/doc/2.6/book/security.html Tests ----- diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 249d4c14f3192..54a76dc551c8b 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -40,7 +40,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Security/Csrf/README.md b/src/Symfony/Component/Security/Csrf/README.md index 95a1062091ed3..89ed66cb5a724 100644 --- a/src/Symfony/Component/Security/Csrf/README.md +++ b/src/Symfony/Component/Security/Csrf/README.md @@ -9,7 +9,7 @@ Resources Documentation: -http://symfony.com/doc/2.5/book/security.html +http://symfony.com/doc/2.6/book/security.html Tests ----- diff --git a/src/Symfony/Component/Security/Csrf/composer.json b/src/Symfony/Component/Security/Csrf/composer.json index 398a2d3c454f4..4daba5ca38b49 100644 --- a/src/Symfony/Component/Security/Csrf/composer.json +++ b/src/Symfony/Component/Security/Csrf/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Security/Http/README.md b/src/Symfony/Component/Security/Http/README.md index c0760d4edae4a..e19af427b84ea 100644 --- a/src/Symfony/Component/Security/Http/README.md +++ b/src/Symfony/Component/Security/Http/README.md @@ -11,7 +11,7 @@ Resources Documentation: -http://symfony.com/doc/2.5/book/security.html +http://symfony.com/doc/2.6/book/security.html Tests ----- diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index c544ad173ab41..812952339211f 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -38,7 +38,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Security/README.md b/src/Symfony/Component/Security/README.md index 5866d129e9fc8..c799a5d843c5c 100644 --- a/src/Symfony/Component/Security/README.md +++ b/src/Symfony/Component/Security/README.md @@ -11,7 +11,7 @@ Resources Documentation: -http://symfony.com/doc/2.5/book/security.html +http://symfony.com/doc/2.6/book/security.html Tests ----- diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index a8a99f553f48f..6d60781bfdb59 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -52,7 +52,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 9e0fe096b07e7..eab354fa91728 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Stopwatch/composer.json b/src/Symfony/Component/Stopwatch/composer.json index 75ed1f60fc050..b424c4ea1a0bc 100644 --- a/src/Symfony/Component/Stopwatch/composer.json +++ b/src/Symfony/Component/Stopwatch/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Templating/composer.json b/src/Symfony/Component/Templating/composer.json index d1dd60d15a2ed..c6b42ba5921dd 100644 --- a/src/Symfony/Component/Templating/composer.json +++ b/src/Symfony/Component/Templating/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Translation/README.md b/src/Symfony/Component/Translation/README.md index 641db8733c3c3..a9e395c54d2ea 100644 --- a/src/Symfony/Component/Translation/README.md +++ b/src/Symfony/Component/Translation/README.md @@ -26,7 +26,7 @@ https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/TranslationServic Documentation: -http://symfony.com/doc/2.5/book/translation.html +http://symfony.com/doc/2.6/book/translation.html You can run the unit tests with the following command: diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 37fef30451385..37248c45400ee 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Validator/README.md b/src/Symfony/Component/Validator/README.md index 6825a2feee198..83eaa1e3ba2cd 100644 --- a/src/Symfony/Component/Validator/README.md +++ b/src/Symfony/Component/Validator/README.md @@ -107,7 +107,7 @@ https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/ValidatorServiceP Documentation: -http://symfony.com/doc/2.5/book/validation.html +http://symfony.com/doc/2.6/book/validation.html JSR-303 Specification: diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 012697b19b3c2..cfc6fbfffec64 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -48,7 +48,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Symfony/Component/Yaml/composer.json b/src/Symfony/Component/Yaml/composer.json index ceabb0c132ec8..2b2d961b7b357 100644 --- a/src/Symfony/Component/Yaml/composer.json +++ b/src/Symfony/Component/Yaml/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } From e40b717b175dd30ed501ef4ec7ec9d0333bbd0bd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 23 May 2014 16:20:40 +0200 Subject: [PATCH 168/323] [Debug] preserve modified error level --- src/Symfony/Component/Debug/ErrorHandler.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 850f7d9c55376..2d414a011bf88 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -225,7 +225,11 @@ public static function unstackErrors() $level = array_pop(self::$stackedErrorLevels); if (null !== $level) { - error_reporting($level); + $e = error_reporting($level); + if ($e !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) { + // If the user changed the error level, do not overwrite it + error_reporting($e); + } } if (empty(self::$stackedErrorLevels)) { From d06b20676291fa0946c4d4667264263572ec7eb1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 25 May 2014 12:15:31 +0200 Subject: [PATCH 169/323] [Debug] throw even in stacking mode to preserve code paths --- src/Symfony/Component/Debug/ErrorHandler.php | 19 ++++++++++--------- .../Debug/Tests/DebugClassLoaderTest.php | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 850f7d9c55376..e3498ccecf043 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -117,7 +117,7 @@ public static function setLogger(LoggerInterface $logger, $channel = 'deprecatio } /** - * @throws ContextErrorException When error_reporting returns error + * @throws \ErrorException When error_reporting returns error */ public function handle($level, $message, $file = 'unknown', $line = 0, $context = array()) { @@ -145,18 +145,19 @@ function ($row) { return true; } } elseif ($this->displayErrors && error_reporting() & $level && $this->level & $level) { - if (self::$stackedErrorLevels) { - self::$stackedErrors[] = func_get_args(); - - return true; - } - if (PHP_VERSION_ID < 50400 && isset($context['GLOBALS']) && is_array($context)) { - unset($context['GLOBALS']); + $c = $context; // Whatever the signature of the method, + unset($c['GLOBALS'], $context); // $context is always a reference in 5.3 + $context = $c; } $exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line); - $exception = new ContextErrorException($exception, 0, $level, $file, $line, $context); + if ($context && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) { + // Checking for class existence is a work around for https://bugs.php.net/42098 + $exception = new ContextErrorException($exception, 0, $level, $file, $line, $context); + } else { + $exception = new \ErrorException($exception, 0, $level, $file, $line); + } if (PHP_VERSION_ID <= 50407 && (PHP_VERSION_ID >= 50400 || PHP_VERSION_ID <= 50317)) { // Exceptions thrown from error handlers are sometimes not caught by the exception diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index a002e22d0bbe2..12224e029f57e 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -99,7 +99,7 @@ public function testStacking() class ChildTestingStacking extends TestingStacking { function foo($bar) {} } '); $this->fail('ContextErrorException expected'); - } catch (ContextErrorException $exception) { + } catch (\ErrorException $exception) { // if an exception is thrown, the test passed restore_error_handler(); restore_exception_handler(); From 682f0a34fe1b9bf251733318d126a43ce0d19586 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 26 May 2014 21:02:40 +0200 Subject: [PATCH 170/323] Revert "bug #10908 [HttpFoundation] implement session locking for PDO (Tobion)" This reverts commit 8c71454f47bbcdf82693a0501acac3e8fe6e08cf, reversing changes made to 735e9a4768962b6d5098733c347cff0df6b9cd36. --- .../stubs/SessionHandlerInterface.php | 12 - .../Storage/Handler/PdoSessionHandler.php | 306 ++++-------------- .../Storage/Handler/PdoSessionHandlerTest.php | 124 +++---- 3 files changed, 115 insertions(+), 327 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php index 9557135bcfbd1..24280e38fca4a 100644 --- a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php +++ b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php @@ -12,14 +12,6 @@ /** * SessionHandlerInterface for PHP < 5.4 * - * The order in which these methods are invoked by PHP are: - * 1. open [session_start] - * 2. read - * 3. gc [optional depending on probability settings: gc_probability / gc_divisor] - * 4. destroy [optional when session_regenerate_id(true) is used] - * 5. write [session_write_close] or destroy [session_destroy] - * 6. close - * * Extensive documentation can be found at php.net, see links: * * @see http://php.net/sessionhandlerinterface @@ -27,7 +19,6 @@ * @see http://php.net/session-set-save-handler * * @author Drak - * @author Tobias Schultze */ interface SessionHandlerInterface { @@ -66,9 +57,6 @@ public function read($sessionId); /** * Writes the session data to the storage. * - * Care, the session ID passed to write() can be different from the one previously - * received in read() when the session ID changed due to session_regenerate_id(). - * * @see http://php.net/sessionhandlerinterface.write * * @param string $sessionId Session ID , see http://php.net/function.session-id diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 4cdf3a89856e8..b831383a24d0b 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -12,19 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * Session handler using a PDO connection to read and write data. - * - * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements - * locking of sessions to prevent loss of data by concurrent access to the same session. - * This means requests for the same session will wait until the other one finished. - * PHPs internal files session handler also works this way. - * - * Attention: Since SQLite does not support row level locks but locks the whole database, - * it means only one session can be accessed at a time. Even different sessions would wait - * for another to finish. So saving session in SQLite should only be considered for - * development or prototypes. - * - * @see http://php.net/sessionhandlerinterface + * PdoSessionHandler. * * @author Fabien Potencier * @author Michael Williams @@ -37,11 +25,6 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $pdo; - /** - * @var string Database driver - */ - private $driver; - /** * @var string Table name */ @@ -62,50 +45,39 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $timeCol; - /** - * @var bool Whether a transaction is active - */ - private $inTransaction = false; - - /** - * @var bool Whether gc() has been called - */ - private $gcCalled = false; - /** * Constructor. * * List of available options: - * * db_table: The name of the table [default: sessions] + * * db_table: The name of the table [required] * * db_id_col: The column where to store the session id [default: sess_id] * * db_data_col: The column where to store the session data [default: sess_data] * * db_time_col: The column where to store the timestamp [default: sess_time] * - * @param \PDO $pdo A \PDO instance - * @param array $options An associative array of DB options + * @param \PDO $pdo A \PDO instance + * @param array $dbOptions An associative array of DB options * - * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + * @throws \InvalidArgumentException When "db_table" option is not provided */ - public function __construct(\PDO $pdo, array $options = array()) + public function __construct(\PDO $pdo, array $dbOptions = array()) { + if (!array_key_exists('db_table', $dbOptions)) { + throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.'); + } if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) { throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); } - $this->pdo = $pdo; - $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); - - $options = array_replace(array( - 'db_table' => 'sessions', + $dbOptions = array_merge(array( 'db_id_col' => 'sess_id', 'db_data_col' => 'sess_data', 'db_time_col' => 'sess_time', - ), $options); + ), $dbOptions); - $this->table = $options['db_table']; - $this->idCol = $options['db_id_col']; - $this->dataCol = $options['db_data_col']; - $this->timeCol = $options['db_time_col']; + $this->table = $dbOptions['db_table']; + $this->idCol = $dbOptions['db_id_col']; + $this->dataCol = $dbOptions['db_data_col']; + $this->timeCol = $dbOptions['db_time_col']; } /** @@ -113,45 +85,34 @@ public function __construct(\PDO $pdo, array $options = array()) */ public function open($savePath, $sessionName) { - $this->gcCalled = false; + return true; + } + /** + * {@inheritdoc} + */ + public function close() + { return true; } /** * {@inheritdoc} */ - public function read($sessionId) + public function destroy($sessionId) { - $this->beginTransaction(); + // delete the record associated with this id + $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; try { - $this->lockSession($sessionId); - - // We need to make sure we do not return session data that is already considered garbage according - // to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. - $maxlifetime = (int) ini_get('session.gc_maxlifetime'); - - $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id AND $this->timeCol > :time"; - $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); $stmt->execute(); - - // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed - $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); - - if ($sessionRows) { - return base64_decode($sessionRows[0][0]); - } - - return ''; } catch (\PDOException $e) { - $this->rollback(); - - throw $e; + throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e); } + + return true; } /** @@ -159,9 +120,16 @@ public function read($sessionId) */ public function gc($maxlifetime) { - // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. - // This way, pruning expired sessions does not block them from being started while the current session is used. - $this->gcCalled = true; + // delete the session records that have expired + $sql = "DELETE FROM $this->table WHERE $this->timeCol < :time"; + + try { + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); + $stmt->execute(); + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e); + } return true; } @@ -169,22 +137,26 @@ public function gc($maxlifetime) /** * {@inheritdoc} */ - public function destroy($sessionId) + public function read($sessionId) { - // delete the record associated with this id - $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; + $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id"; try { $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->execute(); - } catch (\PDOException $e) { - $this->rollback(); - throw $e; - } + // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed + $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); - return true; + if ($sessionRows) { + return base64_decode($sessionRows[0][0]); + } + + return ''; + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e); + } } /** @@ -195,10 +167,8 @@ public function write($sessionId, $data) // Session data can contain non binary safe characters so we need to encode it. $encoded = base64_encode($data); - // The session ID can be different from the one previously received in read() - // when the session ID changed due to session_regenerate_id(). So we have to - // do an insert or update even if we created a row in read() for locking. // We use a MERGE SQL query when supported by the database. + // Otherwise we have to use a transactional DELETE followed by INSERT to prevent duplicate entries under high concurrency. try { $mergeSql = $this->getMergeSql(); @@ -213,18 +183,15 @@ public function write($sessionId, $data) return true; } - $updateStmt = $this->pdo->prepare( - "UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id" - ); - $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); - $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); - $updateStmt->execute(); - - // Since we have a lock on the session, this is safe to do. Otherwise it would be prone to - // race conditions in high concurrency. And if it's a regenerated session ID it should be - // unique anyway. - if (!$updateStmt->rowCount()) { + $this->pdo->beginTransaction(); + + try { + $deleteStmt = $this->pdo->prepare( + "DELETE FROM $this->table WHERE $this->idCol = :id" + ); + $deleteStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $deleteStmt->execute(); + $insertStmt = $this->pdo->prepare( "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)" ); @@ -232,153 +199,18 @@ public function write($sessionId, $data) $insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); $insertStmt->execute(); - } - } catch (\PDOException $e) { - $this->rollback(); - - throw $e; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function close() - { - $this->commit(); - - if ($this->gcCalled) { - $maxlifetime = (int) ini_get('session.gc_maxlifetime'); - - // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->timeCol <= :time"; - $stmt = $this->pdo->prepare($sql); - $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); - $stmt->execute(); - } - - return true; - } - - /** - * Helper method to begin a transaction. - * - * Since SQLite does not support row level locks, we have to acquire a reserved lock - * on the database immediately. Because of https://bugs.php.net/42766 we have to create - * such a transaction manually which also means we cannot use PDO::commit or - * PDO::rollback or PDO::inTransaction for SQLite. - */ - private function beginTransaction() - { - if ($this->inTransaction) { - $this->rollback(); - - throw new \BadMethodCallException( - 'Session handler methods have been invoked in wrong sequence. ' . - 'Expected sequence: open() -> read() -> destroy() / write() -> close()'); - } - - if ('sqlite' === $this->driver) { - $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); - } else { - $this->pdo->beginTransaction(); - } - $this->inTransaction = true; - } - - /** - * Helper method to commit a transaction. - */ - private function commit() - { - if ($this->inTransaction) { - try { - // commit read-write transaction which also releases the lock - if ('sqlite' === $this->driver) { - $this->pdo->exec('COMMIT'); - } else { - $this->pdo->commit(); - } - $this->inTransaction = false; + $this->pdo->commit(); } catch (\PDOException $e) { - $this->rollback(); + $this->pdo->rollback(); throw $e; } - } - } - - /** - * Helper method to rollback a transaction. - */ - private function rollback() - { - // We only need to rollback if we are in a transaction. Otherwise the resulting - // error would hide the real problem why rollback was called. We might not be - // in a transaction when two callbacks (e.g. destroy and write) are invoked that - // both fail. - if ($this->inTransaction) { - if ('sqlite' === $this->driver) { - $this->pdo->exec('ROLLBACK'); - } else { - $this->pdo->rollback(); - } - $this->inTransaction = false; - } - } - - /** - * Exclusively locks the row so other concurrent requests on the same session will block. - * - * This prevents loss of data by keeping the data consistent between read() and write(). - * We do not use SELECT FOR UPDATE because it does not lock non-existent rows. And a following - * INSERT when not found can result in a deadlock for one connection. - * - * @param string $sessionId Session ID - */ - private function lockSession($sessionId) - { - switch ($this->driver) { - case 'mysql': - // will also lock the row when actually nothing got updated (id = id) - $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "ON DUPLICATE KEY UPDATE $this->idCol = $this->idCol"; - break; - case 'oci': - // DUAL is Oracle specific dummy table - $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . - "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol"; - break; - case 'sqlsrv': - // MS SQL Server requires MERGE be terminated by semicolon - $sql = "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . - "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol;"; - break; - case 'pgsql': - // obtain an exclusive transaction level advisory lock - $sql = 'SELECT pg_advisory_xact_lock(:lock_id)'; - $stmt = $this->pdo->prepare($sql); - $stmt->bindValue(':lock_id', hexdec(substr($sessionId, 0, 15)), \PDO::PARAM_INT); - $stmt->execute(); - - return; - default: - return; + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e); } - // We create a DML lock for the session by inserting empty data or updating the row. - // This is safer than an application level advisory lock because it also prevents concurrent modification - // of the session from other parts of the application. - $stmt = $this->pdo->prepare($sql); - $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $stmt->bindValue(':data', '', \PDO::PARAM_STR); - $stmt->bindValue(':time', time(), \PDO::PARAM_INT); - $stmt->execute(); + return true; } /** @@ -388,7 +220,9 @@ private function lockSession($sessionId) */ private function getMergeSql() { - switch ($this->driver) { + $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + + switch ($driver) { case 'mysql': return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)"; @@ -396,12 +230,12 @@ private function getMergeSql() // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data"; case 'sqlsrv': // MS SQL Server requires MERGE be terminated by semicolon return "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data;"; case 'sqlite': return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 109addbcbd93a..e465f398984da 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -29,108 +29,74 @@ protected function setUp() $this->pdo->exec($sql); } - /** - * @expectedException \InvalidArgumentException - */ + public function testIncompleteOptions() + { + $this->setExpectedException('InvalidArgumentException'); + $storage = new PdoSessionHandler($this->pdo, array()); + } + public function testWrongPdoErrMode() { - $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); + $pdo = new \PDO("sqlite::memory:"); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); + $pdo->exec("CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)"); - $storage = new PdoSessionHandler($this->pdo); + $this->setExpectedException('InvalidArgumentException'); + $storage = new PdoSessionHandler($pdo, array('db_table' => 'sessions')); } - /** - * @expectedException \RuntimeException - */ - public function testInexistentTable() + public function testWrongTableOptionsWrite() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'inexistent_table')); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->write('id', 'data'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); + $this->setExpectedException('RuntimeException'); + $storage->write('foo', 'bar'); } - public function testReadWriteRead() + public function testWrongTableOptionsRead() { - $storage = new PdoSessionHandler($this->pdo); - $storage->open('', 'sid'); - $this->assertSame('', $storage->read('id'), 'New session returns empty string data'); - $storage->write('id', 'data'); - $storage->close(); - - $storage->open('', 'sid'); - $this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); + $this->setExpectedException('RuntimeException'); + $storage->read('foo', 'bar'); } - /** - * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace) - */ - public function testWriteDifferentSessionIdThanRead() + public function testWriteRead() { - $storage = new PdoSessionHandler($this->pdo); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->destroy('id'); - $storage->write('new_id', 'data_of_new_session_id'); - $storage->close(); - - $storage->open('', 'sid'); - $this->assertSame('data_of_new_session_id', $storage->read('new_id'), 'Data of regenerated session id is available'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $storage->write('foo', 'bar'); + $this->assertEquals('bar', $storage->read('foo'), 'written value can be read back correctly'); } - /** - * @expectedException \BadMethodCallException - */ - public function testWrongUsage() + public function testMultipleInstances() { - $storage = new PdoSessionHandler($this->pdo); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->read('id'); + $storage1 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $storage1->write('foo', 'bar'); + + $storage2 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $this->assertEquals('bar', $storage2->read('foo'), 'values persist between instances'); } public function testSessionDestroy() { - $storage = new PdoSessionHandler($this->pdo); - - $storage->open('', 'sid'); - $storage->read('id'); - $storage->write('id', 'data'); - $storage->close(); - $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); - - $storage->open('', 'sid'); - $storage->read('id'); - $storage->destroy('id'); - $storage->close(); - $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); - - $storage->open('', 'sid'); - $this->assertSame('', $storage->read('id'), 'Destroyed session returns empty string'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $storage->write('foo', 'bar'); + $this->assertCount(1, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); + + $storage->destroy('foo'); + + $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); } public function testSessionGC() { - $previousLifeTime = ini_set('session.gc_maxlifetime', 0); - $storage = new PdoSessionHandler($this->pdo); - - $storage->open('', 'sid'); - $storage->read('id'); - $storage->write('id', 'data'); - $storage->close(); - $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); - - $storage->open('', 'sid'); - $this->assertSame('', $storage->read('id'), 'Session already considered garbage, so not returning data even if it is not pruned yet'); - $storage->gc(0); - $storage->close(); - $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); - - ini_set('session.gc_maxlifetime', $previousLifeTime); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + + $storage->write('foo', 'bar'); + $storage->write('baz', 'bar'); + + $this->assertCount(2, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); + + $storage->gc(-1); + $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); } public function testGetConnection() From 76ccb280fbb966d3d1c612a8dff600cc2228bc58 Mon Sep 17 00:00:00 2001 From: Tobias Schultze Date: Thu, 15 May 2014 19:19:48 +0200 Subject: [PATCH 171/323] [HttpFoundation] implement session locking for PDO --- .../stubs/SessionHandlerInterface.php | 12 + .../Storage/Handler/PdoSessionHandler.php | 306 ++++++++++++++---- .../Storage/Handler/PdoSessionHandlerTest.php | 124 ++++--- 3 files changed, 327 insertions(+), 115 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php index 24280e38fca4a..9557135bcfbd1 100644 --- a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php +++ b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php @@ -12,6 +12,14 @@ /** * SessionHandlerInterface for PHP < 5.4 * + * The order in which these methods are invoked by PHP are: + * 1. open [session_start] + * 2. read + * 3. gc [optional depending on probability settings: gc_probability / gc_divisor] + * 4. destroy [optional when session_regenerate_id(true) is used] + * 5. write [session_write_close] or destroy [session_destroy] + * 6. close + * * Extensive documentation can be found at php.net, see links: * * @see http://php.net/sessionhandlerinterface @@ -19,6 +27,7 @@ * @see http://php.net/session-set-save-handler * * @author Drak + * @author Tobias Schultze */ interface SessionHandlerInterface { @@ -57,6 +66,9 @@ public function read($sessionId); /** * Writes the session data to the storage. * + * Care, the session ID passed to write() can be different from the one previously + * received in read() when the session ID changed due to session_regenerate_id(). + * * @see http://php.net/sessionhandlerinterface.write * * @param string $sessionId Session ID , see http://php.net/function.session-id diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index b831383a24d0b..4cdf3a89856e8 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -12,7 +12,19 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * PdoSessionHandler. + * Session handler using a PDO connection to read and write data. + * + * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements + * locking of sessions to prevent loss of data by concurrent access to the same session. + * This means requests for the same session will wait until the other one finished. + * PHPs internal files session handler also works this way. + * + * Attention: Since SQLite does not support row level locks but locks the whole database, + * it means only one session can be accessed at a time. Even different sessions would wait + * for another to finish. So saving session in SQLite should only be considered for + * development or prototypes. + * + * @see http://php.net/sessionhandlerinterface * * @author Fabien Potencier * @author Michael Williams @@ -25,6 +37,11 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $pdo; + /** + * @var string Database driver + */ + private $driver; + /** * @var string Table name */ @@ -45,39 +62,50 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $timeCol; + /** + * @var bool Whether a transaction is active + */ + private $inTransaction = false; + + /** + * @var bool Whether gc() has been called + */ + private $gcCalled = false; + /** * Constructor. * * List of available options: - * * db_table: The name of the table [required] + * * db_table: The name of the table [default: sessions] * * db_id_col: The column where to store the session id [default: sess_id] * * db_data_col: The column where to store the session data [default: sess_data] * * db_time_col: The column where to store the timestamp [default: sess_time] * - * @param \PDO $pdo A \PDO instance - * @param array $dbOptions An associative array of DB options + * @param \PDO $pdo A \PDO instance + * @param array $options An associative array of DB options * - * @throws \InvalidArgumentException When "db_table" option is not provided + * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION */ - public function __construct(\PDO $pdo, array $dbOptions = array()) + public function __construct(\PDO $pdo, array $options = array()) { - if (!array_key_exists('db_table', $dbOptions)) { - throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.'); - } if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) { throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); } + $this->pdo = $pdo; - $dbOptions = array_merge(array( + $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + + $options = array_replace(array( + 'db_table' => 'sessions', 'db_id_col' => 'sess_id', 'db_data_col' => 'sess_data', 'db_time_col' => 'sess_time', - ), $dbOptions); + ), $options); - $this->table = $dbOptions['db_table']; - $this->idCol = $dbOptions['db_id_col']; - $this->dataCol = $dbOptions['db_data_col']; - $this->timeCol = $dbOptions['db_time_col']; + $this->table = $options['db_table']; + $this->idCol = $options['db_id_col']; + $this->dataCol = $options['db_data_col']; + $this->timeCol = $options['db_time_col']; } /** @@ -85,34 +113,45 @@ public function __construct(\PDO $pdo, array $dbOptions = array()) */ public function open($savePath, $sessionName) { - return true; - } + $this->gcCalled = false; - /** - * {@inheritdoc} - */ - public function close() - { return true; } /** * {@inheritdoc} */ - public function destroy($sessionId) + public function read($sessionId) { - // delete the record associated with this id - $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; + $this->beginTransaction(); try { + $this->lockSession($sessionId); + + // We need to make sure we do not return session data that is already considered garbage according + // to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. + $maxlifetime = (int) ini_get('session.gc_maxlifetime'); + + $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id AND $this->timeCol > :time"; + $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); $stmt->execute(); + + // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed + $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); + + if ($sessionRows) { + return base64_decode($sessionRows[0][0]); + } + + return ''; } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e); - } + $this->rollback(); - return true; + throw $e; + } } /** @@ -120,16 +159,9 @@ public function destroy($sessionId) */ public function gc($maxlifetime) { - // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->timeCol < :time"; - - try { - $stmt = $this->pdo->prepare($sql); - $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); - $stmt->execute(); - } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e); - } + // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. + // This way, pruning expired sessions does not block them from being started while the current session is used. + $this->gcCalled = true; return true; } @@ -137,26 +169,22 @@ public function gc($maxlifetime) /** * {@inheritdoc} */ - public function read($sessionId) + public function destroy($sessionId) { - $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id"; + // delete the record associated with this id + $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; try { $stmt = $this->pdo->prepare($sql); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->execute(); - - // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed - $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); - - if ($sessionRows) { - return base64_decode($sessionRows[0][0]); - } - - return ''; } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e); + $this->rollback(); + + throw $e; } + + return true; } /** @@ -167,8 +195,10 @@ public function write($sessionId, $data) // Session data can contain non binary safe characters so we need to encode it. $encoded = base64_encode($data); + // The session ID can be different from the one previously received in read() + // when the session ID changed due to session_regenerate_id(). So we have to + // do an insert or update even if we created a row in read() for locking. // We use a MERGE SQL query when supported by the database. - // Otherwise we have to use a transactional DELETE followed by INSERT to prevent duplicate entries under high concurrency. try { $mergeSql = $this->getMergeSql(); @@ -183,15 +213,18 @@ public function write($sessionId, $data) return true; } - $this->pdo->beginTransaction(); - - try { - $deleteStmt = $this->pdo->prepare( - "DELETE FROM $this->table WHERE $this->idCol = :id" - ); - $deleteStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $deleteStmt->execute(); - + $updateStmt = $this->pdo->prepare( + "UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id" + ); + $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); + $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->execute(); + + // Since we have a lock on the session, this is safe to do. Otherwise it would be prone to + // race conditions in high concurrency. And if it's a regenerated session ID it should be + // unique anyway. + if (!$updateStmt->rowCount()) { $insertStmt = $this->pdo->prepare( "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)" ); @@ -199,18 +232,153 @@ public function write($sessionId, $data) $insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); $insertStmt->execute(); + } + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function close() + { + $this->commit(); + + if ($this->gcCalled) { + $maxlifetime = (int) ini_get('session.gc_maxlifetime'); + + // delete the session records that have expired + $sql = "DELETE FROM $this->table WHERE $this->timeCol <= :time"; - $this->pdo->commit(); + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); + $stmt->execute(); + } + + return true; + } + + /** + * Helper method to begin a transaction. + * + * Since SQLite does not support row level locks, we have to acquire a reserved lock + * on the database immediately. Because of https://bugs.php.net/42766 we have to create + * such a transaction manually which also means we cannot use PDO::commit or + * PDO::rollback or PDO::inTransaction for SQLite. + */ + private function beginTransaction() + { + if ($this->inTransaction) { + $this->rollback(); + + throw new \BadMethodCallException( + 'Session handler methods have been invoked in wrong sequence. ' . + 'Expected sequence: open() -> read() -> destroy() / write() -> close()'); + } + + if ('sqlite' === $this->driver) { + $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); + } else { + $this->pdo->beginTransaction(); + } + $this->inTransaction = true; + } + + /** + * Helper method to commit a transaction. + */ + private function commit() + { + if ($this->inTransaction) { + try { + // commit read-write transaction which also releases the lock + if ('sqlite' === $this->driver) { + $this->pdo->exec('COMMIT'); + } else { + $this->pdo->commit(); + } + $this->inTransaction = false; } catch (\PDOException $e) { - $this->pdo->rollback(); + $this->rollback(); throw $e; } - } catch (\PDOException $e) { - throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e); } + } - return true; + /** + * Helper method to rollback a transaction. + */ + private function rollback() + { + // We only need to rollback if we are in a transaction. Otherwise the resulting + // error would hide the real problem why rollback was called. We might not be + // in a transaction when two callbacks (e.g. destroy and write) are invoked that + // both fail. + if ($this->inTransaction) { + if ('sqlite' === $this->driver) { + $this->pdo->exec('ROLLBACK'); + } else { + $this->pdo->rollback(); + } + $this->inTransaction = false; + } + } + + /** + * Exclusively locks the row so other concurrent requests on the same session will block. + * + * This prevents loss of data by keeping the data consistent between read() and write(). + * We do not use SELECT FOR UPDATE because it does not lock non-existent rows. And a following + * INSERT when not found can result in a deadlock for one connection. + * + * @param string $sessionId Session ID + */ + private function lockSession($sessionId) + { + switch ($this->driver) { + case 'mysql': + // will also lock the row when actually nothing got updated (id = id) + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . + "ON DUPLICATE KEY UPDATE $this->idCol = $this->idCol"; + break; + case 'oci': + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . + "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol"; + break; + case 'sqlsrv': + // MS SQL Server requires MERGE be terminated by semicolon + $sql = "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . + "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol;"; + break; + case 'pgsql': + // obtain an exclusive transaction level advisory lock + $sql = 'SELECT pg_advisory_xact_lock(:lock_id)'; + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':lock_id', hexdec(substr($sessionId, 0, 15)), \PDO::PARAM_INT); + $stmt->execute(); + + return; + default: + return; + } + + // We create a DML lock for the session by inserting empty data or updating the row. + // This is safer than an application level advisory lock because it also prevents concurrent modification + // of the session from other parts of the application. + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindValue(':data', '', \PDO::PARAM_STR); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); } /** @@ -220,9 +388,7 @@ public function write($sessionId, $data) */ private function getMergeSql() { - $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); - - switch ($driver) { + switch ($this->driver) { case 'mysql': return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)"; @@ -230,12 +396,12 @@ private function getMergeSql() // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time"; case 'sqlsrv': // MS SQL Server requires MERGE be terminated by semicolon return "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data;"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;"; case 'sqlite': return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index e465f398984da..109addbcbd93a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -29,74 +29,108 @@ protected function setUp() $this->pdo->exec($sql); } - public function testIncompleteOptions() - { - $this->setExpectedException('InvalidArgumentException'); - $storage = new PdoSessionHandler($this->pdo, array()); - } - + /** + * @expectedException \InvalidArgumentException + */ public function testWrongPdoErrMode() { - $pdo = new \PDO("sqlite::memory:"); - $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); - $pdo->exec("CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)"); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); - $this->setExpectedException('InvalidArgumentException'); - $storage = new PdoSessionHandler($pdo, array('db_table' => 'sessions')); + $storage = new PdoSessionHandler($this->pdo); } - public function testWrongTableOptionsWrite() + /** + * @expectedException \RuntimeException + */ + public function testInexistentTable() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); - $this->setExpectedException('RuntimeException'); - $storage->write('foo', 'bar'); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'inexistent_table')); + $storage->open('', 'sid'); + $storage->read('id'); + $storage->write('id', 'data'); + $storage->close(); } - public function testWrongTableOptionsRead() + public function testReadWriteRead() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); - $this->setExpectedException('RuntimeException'); - $storage->read('foo', 'bar'); + $storage = new PdoSessionHandler($this->pdo); + $storage->open('', 'sid'); + $this->assertSame('', $storage->read('id'), 'New session returns empty string data'); + $storage->write('id', 'data'); + $storage->close(); + + $storage->open('', 'sid'); + $this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly'); + $storage->close(); } - public function testWriteRead() + /** + * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace) + */ + public function testWriteDifferentSessionIdThanRead() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage->write('foo', 'bar'); - $this->assertEquals('bar', $storage->read('foo'), 'written value can be read back correctly'); + $storage = new PdoSessionHandler($this->pdo); + $storage->open('', 'sid'); + $storage->read('id'); + $storage->destroy('id'); + $storage->write('new_id', 'data_of_new_session_id'); + $storage->close(); + + $storage->open('', 'sid'); + $this->assertSame('data_of_new_session_id', $storage->read('new_id'), 'Data of regenerated session id is available'); + $storage->close(); } - public function testMultipleInstances() + /** + * @expectedException \BadMethodCallException + */ + public function testWrongUsage() { - $storage1 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage1->write('foo', 'bar'); - - $storage2 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $this->assertEquals('bar', $storage2->read('foo'), 'values persist between instances'); + $storage = new PdoSessionHandler($this->pdo); + $storage->open('', 'sid'); + $storage->read('id'); + $storage->read('id'); } public function testSessionDestroy() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage->write('foo', 'bar'); - $this->assertCount(1, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); - - $storage->destroy('foo'); - - $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); + $storage = new PdoSessionHandler($this->pdo); + + $storage->open('', 'sid'); + $storage->read('id'); + $storage->write('id', 'data'); + $storage->close(); + $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + $storage->open('', 'sid'); + $storage->read('id'); + $storage->destroy('id'); + $storage->close(); + $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + $storage->open('', 'sid'); + $this->assertSame('', $storage->read('id'), 'Destroyed session returns empty string'); + $storage->close(); } public function testSessionGC() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - - $storage->write('foo', 'bar'); - $storage->write('baz', 'bar'); - - $this->assertCount(2, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); - - $storage->gc(-1); - $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); + $previousLifeTime = ini_set('session.gc_maxlifetime', 0); + $storage = new PdoSessionHandler($this->pdo); + + $storage->open('', 'sid'); + $storage->read('id'); + $storage->write('id', 'data'); + $storage->close(); + $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + $storage->open('', 'sid'); + $this->assertSame('', $storage->read('id'), 'Session already considered garbage, so not returning data even if it is not pruned yet'); + $storage->gc(0); + $storage->close(); + $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + + ini_set('session.gc_maxlifetime', $previousLifeTime); } public function testGetConnection() From ffe53a31a37699ba9f060bcd19856b3b6effd622 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 28 May 2014 03:20:43 +0200 Subject: [PATCH 172/323] updated CHANGELOG for 2.5.0-RC1 --- CHANGELOG-2.5.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CHANGELOG-2.5.md b/CHANGELOG-2.5.md index dabbff17a044f..a2d6a71b76e9c 100644 --- a/CHANGELOG-2.5.md +++ b/CHANGELOG-2.5.md @@ -7,6 +7,55 @@ in 2.5 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.5.0...v2.5.1 +* 2.5.0-RC1 (2014-05-28) + + * bug #10979 Make rootPath part of regex greedy (artursvonda) + * bug #10995 [TwigBridge][Trans]set %count% only on transChoice from the current context. (aitboudad) + * bug #10989 [Debug] throw even in stacking mode to preserve code paths (nicolas-grekas) + * bug #10987 [DomCrawler] Fixed a forgotten case of complex XPath queries (stof) + * feature #10930 [Process] Deprecate using values that are not string for Process::setStdin and ProcessBuilder::setInput (romainneutron) + * bug #10971 [Process] Fix conflicts between latest 2.3 fix and 2.5 deprecation (romainneutron) + * feature #10932 [Process] Deprecate Process::setStdin in favor of Process::setInput (romainneutron) + * bug #10849 [WIP][Finder] Fix wrong implementation on sortable callback comparator (ProPheT777) + * bug #10929 [Process] Add validation on Process input (romainneutron) + * bug #10946 [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations (webmozart) + * bug #10958 [DomCrawler] Fixed filterXPath() chaining loosing the parent DOM nodes (stof, robbertkl) + * bug #10953 [HttpKernel] fixed file uploads in functional tests without file selected (realmfoo) + * feature #10941 [Debug] cleanup interfaces before 2.5-final (nicolas-grekas) + * bug #10947 [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations (webmozart) + * bug #10937 [HttpKernel] Fix "absolute path" when we look to the cache directory (BenoitLeveque) + * bug #10933 Changed the default value of checkbox and radio to match the HTML spec (stof) + * bug #10927 [DomCrawler] Changed typehints form DomNode to DomElement (stof) + * bug #10908 [HttpFoundation] implement session locking for PDO (Tobion) + * bug #10894 [HttpKernel] removed absolute paths from the generated container (fabpot) + * bug #10926 [DomCrawler] Fixed the initial state for options without value attribute (stof) + * bug #10925 [DomCrawler] Fixed the handling of boolean attributes in ChoiceFormField (stof) + * feature #10882 Fix issue #10867 (umpirsky) + * bug #10902 [Yaml] Fixed YAML Parser does not ignore duplicate keys, violating YAML spec. (sun) + * feature #10912 [Form] Added support for injecting HttpFoundation's Request in ServerParams for the Validator extension (csarrazi) + * bug #10777 [Form] Automatically add step attribute to HTML5 time widgets to display seconds if needed (tucksaun) + * bug #10909 [PropertyAccess] Fixed plurals for -ves words (csarrazi) + * bug #10904 [HttpKernel] Replace sha1 with sha256 in recently added tests (jakzal) + * bug #10899 Explicitly define the encoding. (jakzal) + * bug #10897 [Console] Fix a console test (jakzal) + * bug #10896 [HttpKernel] Fixed cache behavior when TTL has expired and a default "global" TTL is defined (alquerci, fabpot) + * bug #10841 [DomCrawler] Fixed image input case sensitive (geoffrey-brier) + * bug #10714 [Console]Improve formatter for double-width character (denkiryokuhatsuden) + * bug #10872 [Form] Fixed TrimListenerTest as of PHP 5.5 (webmozart) + * feature #10880 [DependencyInjection] GraphvizDumper now displays unresolved parameters (rosstuck) + * bug #10876 [Console] Make `Helper\Table::setStyle()` chainable again (stloyd) + * bug #10762 [BrowserKit] Allow URLs that don't contain a path when creating a cookie from a string (thewilkybarkid) + * bug #10861 [Debug] enhance perf of DebugClassLoader (nicolas-grekas) + * bug #10863 [Security] Add check for supported attributes in AclVoter (artursvonda) + * bug #10854 [Debug] fix handling deprecated warnings and stacked errors turned into exceptions (nicolas-grekas) + * feature #10843 [TwigBridge] Added compile-time issues checking in twig:lint command (maxromanovsky) + * feature #10829 Fix issue 9172 (umpirsky) + * bug #10833 [TwigBridge][Transchoice] set %count% from the current context. (aitboudad) + * bug #10820 [WebProfilerBundle] Fixed profiler seach/homepage with empty token (tucksaun) + * bug #10809 Fixed composer to include config component for mocks in phpunit (jpauli) + * bug #10815 Fixed issue #5427 (umpirsky) + * bug #10817 [Debug] fix #10313: FlattenException not found (nicolas-grekas) + * 2.5.0-BETA2 (2014-04-29) * bug #10803 [Debug] fix ErrorHandlerTest when context is not an array (nicolas-grekas) From 515fbe9d95c89245ffc9f00c34031147963ed6a0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 28 May 2014 03:21:58 +0200 Subject: [PATCH 173/323] updated VERSION for 2.5.0-RC1 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 7685d08dfc92a..faad3cfcb43fc 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-DEV'; + const VERSION = '2.5.0-RC1'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = 'RC1'; /** * Constructor. From 082006278c4b3dad056faceac403be42f3e9d439 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 May 2014 01:33:40 +0200 Subject: [PATCH 174/323] bumped Symfony version to 2.5.0 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index faad3cfcb43fc..7685d08dfc92a 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-RC1'; + const VERSION = '2.5.0-DEV'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'RC1'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From 6b3be67d3ef8ef8cf934bf8d4da282c5904371a1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 29 May 2014 01:38:22 +0200 Subject: [PATCH 175/323] fixed CHANGELOG for 2.5 --- CHANGELOG-2.5.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG-2.5.md b/CHANGELOG-2.5.md index a2d6a71b76e9c..19c7b211995c1 100644 --- a/CHANGELOG-2.5.md +++ b/CHANGELOG-2.5.md @@ -26,7 +26,6 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * bug #10937 [HttpKernel] Fix "absolute path" when we look to the cache directory (BenoitLeveque) * bug #10933 Changed the default value of checkbox and radio to match the HTML spec (stof) * bug #10927 [DomCrawler] Changed typehints form DomNode to DomElement (stof) - * bug #10908 [HttpFoundation] implement session locking for PDO (Tobion) * bug #10894 [HttpKernel] removed absolute paths from the generated container (fabpot) * bug #10926 [DomCrawler] Fixed the initial state for options without value attribute (stof) * bug #10925 [DomCrawler] Fixed the handling of boolean attributes in ChoiceFormField (stof) From 0df6ebf595f8cccf86128e1d1ce5beed59a0839b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 31 May 2014 20:45:43 +0200 Subject: [PATCH 176/323] updated CHANGELOG for 2.5.0 --- CHANGELOG-2.5.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG-2.5.md b/CHANGELOG-2.5.md index 19c7b211995c1..3bc0c9ed4f500 100644 --- a/CHANGELOG-2.5.md +++ b/CHANGELOG-2.5.md @@ -7,6 +7,11 @@ in 2.5 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.5.0...v2.5.1 +* 2.5.0 (2014-05-31) + + * bug #11014 [Validator] Remove property and method targets from the optional and required constraints (jakzal) + * bug #10983 [DomCrawler] Fixed charset detection in html5 meta charset tag (77web) + * 2.5.0-RC1 (2014-05-28) * bug #10979 Make rootPath part of regex greedy (artursvonda) From 59365832a09a1d914d14cbca6a21a7f572760c3b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 31 May 2014 20:45:50 +0200 Subject: [PATCH 177/323] updated VERSION for 2.5.0 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 7685d08dfc92a..24fc68fd7a57d 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0-DEV'; + const VERSION = '2.5.0'; const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; const RELEASE_VERSION = '0'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = ''; /** * Constructor. From 9c60a8582eed84cad8e1b280c31f4051a18aba42 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Jun 2014 17:15:34 +0200 Subject: [PATCH 178/323] bumped Symfony version to 2.5.1 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 24fc68fd7a57d..694135bfe9384 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -59,12 +59,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.5.0'; - const VERSION_ID = '20500'; + const VERSION = '2.5.1-DEV'; + const VERSION_ID = '20501'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '5'; - const RELEASE_VERSION = '0'; - const EXTRA_VERSION = ''; + const RELEASE_VERSION = '1'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From 8e9cc350c2739e1c0ff396ceab6785d4e2889ddd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2014 08:59:12 +0200 Subject: [PATCH 179/323] [Debug] fix wrong case mismatch exception --- src/Symfony/Component/Debug/DebugClassLoader.php | 2 +- src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php | 5 +++++ src/Symfony/Component/Debug/Tests/Fixtures/ClassAlias.php | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Debug/Tests/Fixtures/ClassAlias.php diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index eb1be7a46dd87..4a02e616cce89 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -175,7 +175,7 @@ public function loadClass($class) $refl = new \ReflectionClass($class); $name = $refl->getName(); - if ($name !== $class) { + if ($name !== $class && 0 === strcasecmp($name, $class)) { throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name)); } } diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 12224e029f57e..396cc98ee9b75 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -151,6 +151,11 @@ public function testNotPsr0Bis() { $this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\NotPSR0bis', true)); } + + public function testClassAlias() + { + $this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\ClassAlias', true)); + } } class ClassLoader diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/ClassAlias.php b/src/Symfony/Component/Debug/Tests/Fixtures/ClassAlias.php new file mode 100644 index 0000000000000..9d6dbaa7124fe --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/ClassAlias.php @@ -0,0 +1,3 @@ + Date: Mon, 26 May 2014 12:16:49 +0200 Subject: [PATCH 180/323] [Debug] enhanced error messages for uncaught exceptions --- .../Bridge/Twig/Extension/CodeExtension.php | 12 ++-- .../Resources/views/Profiler/admin.html.twig | 4 +- src/Symfony/Component/Debug/CHANGELOG.md | 5 ++ src/Symfony/Component/Debug/ErrorHandler.php | 9 ++- .../Component/Debug/ExceptionHandler.php | 39 +++++++----- .../ClassNotFoundFatalErrorHandler.php | 30 ++++----- .../UndefinedFunctionFatalErrorHandler.php | 25 +++----- .../UndefinedMethodFatalErrorHandler.php | 10 ++- .../Debug/Tests/ErrorHandlerTest.php | 61 ++----------------- .../Tests/Exception/FlattenExceptionTest.php | 4 +- .../ClassNotFoundFatalErrorHandlerTest.php | 10 +-- ...UndefinedFunctionFatalErrorHandlerTest.php | 8 +-- .../UndefinedMethodFatalErrorHandlerTest.php | 6 +- 13 files changed, 90 insertions(+), 133 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index 763f315f8ab5c..93d99f936c740 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -36,7 +36,7 @@ class CodeExtension extends \Twig_Extension public function __construct($fileLinkFormat, $rootDir, $charset) { $this->fileLinkFormat = empty($fileLinkFormat) ? ini_get('xdebug.file_link_format') : $fileLinkFormat; - $this->rootDir = str_replace('\\', '/', $rootDir).'/'; + $this->rootDir = str_replace('\\', '/', dirname($rootDir)).'/'; $this->charset = $charset; } @@ -164,12 +164,14 @@ public function fileExcerpt($file, $line) */ public function formatFile($file, $line, $text = null) { + $file = trim($file); + if (null === $text) { - $file = trim($file); - $text = $file; + $text = str_replace('\\', '/', $file); if (0 === strpos($text, $this->rootDir)) { - $text = str_replace($this->rootDir, '', str_replace('\\', '/', $text)); - $text = sprintf('kernel.root_dir/%s', $this->rootDir, $text); + $text = substr($text, strlen($this->rootDir)); + $text = explode('/', $text, 2); + $text = sprintf('%s%s', $this->rootDir, $text[0], isset($text[1]) ? '/'.$text[1] : ''); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig index 3ca28ff1629c0..54140171726dc 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/admin.html.twig @@ -13,8 +13,8 @@ » Export {% endif %} - » 
-
+ » 
+