diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php
index 259510137480..1cd0b10f595f 100644
--- a/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php
+++ b/src/Symfony/Component/Form/Extension/Validator/Type/FieldTypeValidatorExtension.php
@@ -38,6 +38,7 @@ public function buildForm(FormBuilder $builder, array $options)
$builder
->setAttribute('validation_groups', $options['validation_groups'])
->setAttribute('validation_constraint', $options['validation_constraint'])
+ ->setAttribute('cascade_validation', $options['cascade_validation'])
->addValidator(new DelegatingValidator($this->validator));
}
@@ -46,6 +47,7 @@ public function getDefaultOptions(array $options)
return array(
'validation_groups' => null,
'validation_constraint' => null,
+ 'cascade_validation' => false,
);
}
diff --git a/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php b/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php
index a8b06724024f..2b7c099403d7 100644
--- a/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php
+++ b/src/Symfony/Component/Form/Extension/Validator/Validator/DelegatingValidator.php
@@ -127,6 +127,29 @@ static public function validateFormData(FormInterface $form, ExecutionContext $c
}
}
+ static public function validateFormChildren(FormInterface $form, ExecutionContext $context)
+ {
+ if ($form->getAttribute('cascade_validation')) {
+ $propertyPath = $context->getPropertyPath();
+ $graphWalker = $context->getGraphWalker();
+
+ // The Execute constraint is called on class level, so we need to
+ // set the property manually
+ $context->setCurrentProperty('children');
+
+ // Adjust the property path accordingly
+ if (!empty($propertyPath)) {
+ $propertyPath .= '.';
+ }
+
+ $propertyPath .= 'children';
+
+ foreach (self::getFormValidationGroups($form) as $group) {
+ $graphWalker->walkReference($form->getChildren(), $group, $propertyPath, true);
+ }
+ }
+ }
+
static protected function getFormValidationGroups(FormInterface $form)
{
$groups = null;
diff --git a/src/Symfony/Component/Form/Resources/config/validation.xml b/src/Symfony/Component/Form/Resources/config/validation.xml
index 5512560c6d4e..7bd69402c094 100644
--- a/src/Symfony/Component/Form/Resources/config/validation.xml
+++ b/src/Symfony/Component/Form/Resources/config/validation.xml
@@ -10,9 +10,10 @@
Symfony\Component\Form\Extension\Validator\Validator\DelegatingValidator
validateFormData
+
+ Symfony\Component\Form\Extension\Validator\Validator\DelegatingValidator
+ validateFormChildren
+
-
-
-
diff --git a/tests/Symfony/Tests/Component/Form/Extension/Validator/Validator/DelegatingValidatorTest.php b/tests/Symfony/Tests/Component/Form/Extension/Validator/Validator/DelegatingValidatorTest.php
index 22701304a62e..1d16892dc67d 100644
--- a/tests/Symfony/Tests/Component/Form/Extension/Validator/Validator/DelegatingValidatorTest.php
+++ b/tests/Symfony/Tests/Component/Form/Extension/Validator/Validator/DelegatingValidatorTest.php
@@ -798,6 +798,81 @@ public function testValidateFormDataDoesNotWalkScalars()
DelegatingValidator::validateFormData($form, $context);
}
+ public function testValidateFormChildren()
+ {
+ $graphWalker = $this->getMockGraphWalker();
+ $metadataFactory = $this->getMockMetadataFactory();
+ $context = new ExecutionContext('Root', $graphWalker, $metadataFactory);
+ $form = $this->getBuilder()
+ ->setAttribute('cascade_validation', true)
+ ->setAttribute('validation_groups', array('group1', 'group2'))
+ ->getForm();
+ $form->add($this->getForm('firstName'));
+
+ $graphWalker->expects($this->at(0))
+ ->method('walkReference')
+ ->with($form->getChildren(), 'group1', 'children', true);
+ $graphWalker->expects($this->at(1))
+ ->method('walkReference')
+ ->with($form->getChildren(), 'group2', 'children', true);
+
+ DelegatingValidator::validateFormChildren($form, $context);
+ }
+
+ public function testValidateFormChildrenAppendsPropertyPath()
+ {
+ $graphWalker = $this->getMockGraphWalker();
+ $metadataFactory = $this->getMockMetadataFactory();
+ $context = new ExecutionContext('Root', $graphWalker, $metadataFactory);
+ $context->setPropertyPath('path');
+ $form = $this->getBuilder()
+ ->setAttribute('cascade_validation', true)
+ ->getForm();
+ $form->add($this->getForm('firstName'));
+
+ $graphWalker->expects($this->once())
+ ->method('walkReference')
+ ->with($form->getChildren(), 'Default', 'path.children', true);
+
+ DelegatingValidator::validateFormChildren($form, $context);
+ }
+
+ public function testValidateFormChildrenSetsCurrentPropertyToData()
+ {
+ $graphWalker = $this->getMockGraphWalker();
+ $metadataFactory = $this->getMockMetadataFactory();
+ $context = new ExecutionContext('Root', $graphWalker, $metadataFactory);
+ $form = $this->getBuilder()
+ ->setAttribute('cascade_validation', true)
+ ->getForm();
+ $form->add($this->getForm('firstName'));
+ $test = $this;
+
+ $graphWalker->expects($this->once())
+ ->method('walkReference')
+ ->will($this->returnCallback(function () use ($context, $test) {
+ $test->assertEquals('children', $context->getCurrentProperty());
+ }));
+
+ DelegatingValidator::validateFormChildren($form, $context);
+ }
+
+ public function testValidateFormChildrenDoesNothingIfDisabled()
+ {
+ $graphWalker = $this->getMockGraphWalker();
+ $metadataFactory = $this->getMockMetadataFactory();
+ $context = new ExecutionContext('Root', $graphWalker, $metadataFactory);
+ $form = $this->getBuilder()
+ ->setAttribute('cascade_validation', false)
+ ->getForm();
+ $form->add($this->getForm('firstName'));
+
+ $graphWalker->expects($this->never())
+ ->method('walkReference');
+
+ DelegatingValidator::validateFormChildren($form, $context);
+ }
+
public function testValidateIgnoresNonRoot()
{
$form = $this->getMockForm();