瀏覽代碼

[Form] Implemented custom mapping of data errors to form fields

Bernhard Schussek 14 年之前
父節點
當前提交
577e106361

+ 3 - 3
src/Symfony/Component/Form/DataMapper/PropertyPathMapper.php

@@ -12,7 +12,7 @@
 namespace Symfony\Component\Form\DataMapper;
 
 use Symfony\Component\Form\FormInterface;
-use Symfony\Component\Form\RecursiveFormIterator;
+use Symfony\Component\Form\VirtualFormIterator;
 use Symfony\Component\Form\Exception\FormException;
 
 class PropertyPathMapper implements DataMapperInterface
@@ -61,7 +61,7 @@ class PropertyPathMapper implements DataMapperInterface
                 throw new FormException(sprintf('Form data should be instance of %s', $this->dataClass));
             }
 
-            $iterator = new RecursiveFormIterator($forms);
+            $iterator = new VirtualFormIterator($forms);
             $iterator = new \RecursiveIteratorIterator($iterator);
 
             foreach ($iterator as $form) {
@@ -81,7 +81,7 @@ class PropertyPathMapper implements DataMapperInterface
 
     public function mapFormsToData(array $forms, &$data)
     {
-        $iterator = new RecursiveFormIterator($forms);
+        $iterator = new VirtualFormIterator($forms);
         $iterator = new \RecursiveIteratorIterator($iterator);
 
         foreach ($iterator as $form) {

+ 8 - 1
src/Symfony/Component/Form/Form.php

@@ -553,13 +553,20 @@ class Form implements \IteratorAggregate, FormInterface
         return $this->children;
     }
 
+    public function hasChildren()
+    {
+        return count($this->children) > 0;
+    }
+
     public function add(FormInterface $child)
     {
         $this->children[$child->getName()] = $child;
 
         $child->setParent($this);
 
-        $this->dataMapper->mapDataToForm($this->getClientData(), $child);
+        if ($this->dataMapper) {
+            $this->dataMapper->mapDataToForm($this->getClientData(), $child);
+        }
     }
 
     public function remove($name)

+ 1 - 1
src/Symfony/Component/Form/FormBuilder.php

@@ -440,7 +440,7 @@ class FormBuilder
                 $builder = $this->build($name, $builder['type'], $builder['options']);
             }
 
-            $fields[$name] = $builder->getForm();
+            $fields[$builder->getName()] = $builder->getForm();
         }
 
         return $fields;

+ 1 - 1
src/Symfony/Component/Form/PropertyPath.php

@@ -59,7 +59,7 @@ class PropertyPath implements \IteratorAggregate
             throw new InvalidPropertyPathException('The property path must not be empty');
         }
 
-        $this->string = $propertyPath;
+        $this->string = (string)$propertyPath;
         $position = 0;
         $remaining = $propertyPath;
 

+ 2 - 0
src/Symfony/Component/Form/Type/FieldType.php

@@ -58,6 +58,7 @@ class FieldType extends AbstractType
             ->setAttribute('by_reference', $options['by_reference'])
             ->setAttribute('property_path', $options['property_path'])
             ->setAttribute('validation_groups', $options['validation_groups'])
+            ->setAttribute('error_mapping', $options['error_mapping'])
             ->setData($options['data'])
             ->setRenderer(new ThemeRenderer($this->theme, $options['template']))
             ->addRendererPlugin(new FieldPlugin())
@@ -82,6 +83,7 @@ class FieldType extends AbstractType
             'by_reference' => true,
             'validation_groups' => true,
             'error_bubbling' => false,
+            'error_mapping' => array(),
         );
     }
 

+ 59 - 42
src/Symfony/Component/Form/Validator/DelegatingValidator.php

@@ -13,16 +13,12 @@ namespace Symfony\Component\Form\Validator;
 
 use Symfony\Component\Form\FormInterface;
 use Symfony\Component\Form\FormError;
-use Symfony\Component\Form\PropertyPath;
-use Symfony\Component\Form\PropertyPathIterator;
+use Symfony\Component\Form\VirtualFormIterator;
+use Symfony\Component\Form\Exception\FormException;
 use Symfony\Component\Validator\ValidatorInterface;
 
 class DelegatingValidator implements FormValidatorInterface
 {
-    const DATA_ERROR = 0;
-
-    const FORM_ERROR = 1;
-
     private $validator;
 
     public function __construct(ValidatorInterface $validator)
@@ -36,68 +32,89 @@ class DelegatingValidator implements FormValidatorInterface
     public function validate(FormInterface $form)
     {
         if ($form->isRoot()) {
+            $mapping = array();
+            $forms = array();
+
+            $this->buildMapping($form, $mapping, $forms);
+            $this->resolveMappingPlaceholders($mapping, $forms);
+
             // Validate the form in group "Default"
             // Validation of the data in the custom group is done by validateData(),
             // which is constrained by the Execute constraint
             if ($violations = $this->validator->validate($form)) {
                 foreach ($violations as $violation) {
-                    $propertyPath = new PropertyPath($violation->getPropertyPath());
-                    $iterator = $propertyPath->getIterator();
+                    $propertyPath = $violation->getPropertyPath();
                     $template = $violation->getMessageTemplate();
                     $parameters = $violation->getMessageParameters();
                     $error = new FormError($template, $parameters);
 
-                    if ($iterator->current() == 'data') {
-                        $iterator->next(); // point at the first data element
-                        $type = self::DATA_ERROR;
-                    } else {
-                        $type = self::FORM_ERROR;
+                    foreach ($mapping as $mappedPath => $child) {
+                        if (preg_match($mappedPath, $propertyPath)) {
+                            $child->addError($error);
+                            continue 2;
+                        }
                     }
 
-                    $this->mapError($error, $form, $iterator, $type);
+                    $form->addError($error);
                 }
             }
         }
     }
 
-    private function mapError(FormError $error, FormInterface $form,
-            PropertyPathIterator $pathIterator, $type)
+    private function buildMapping(FormInterface $form, array &$mapping,
+            array &$forms, $namePath = '', $formPath = '', $dataPath = 'data')
     {
-        if (null !== $pathIterator && $form instanceof FormInterface) {
-            if ($type === self::FORM_ERROR && $pathIterator->hasNext()) {
-                $pathIterator->next();
+        if ($namePath) {
+            $namePath .= '.';
+        }
 
-                if ($pathIterator->isProperty() && $pathIterator->current() === 'forms') {
-                    $pathIterator->next();
-                }
+        if ($formPath) {
+            $formPath .= '.';
+        }
 
-                if ($form->has($pathIterator->current())) {
-                    $child = $form->get($pathIterator->current());
+        $iterator = new VirtualFormIterator($form->getChildren());
+        $iterator = new \RecursiveIteratorIterator($iterator);
 
-                    $this->mapError($error, $child, $pathIterator, $type);
+        foreach ($iterator as $child) {
+            $path = (string)$child->getAttribute('property_path');
+            $parts = explode('.', $path, 2);
 
-                    return;
-                }
-            } else if ($type === self::DATA_ERROR) {
-                $iterator = new RecursiveFormIterator($form);
-                $iterator = new \RecursiveIteratorIterator($iterator);
+            $nestedNamePath = $namePath . $child->getName();
+            $nestedFormPath = $formPath . 'children[' . $parts[0] . ']';
 
-                foreach ($iterator as $child) {
-                    if (null !== ($childPath = $child->getAttribute('property_path'))) {
-                        if ($childPath->getElement(0) === $pathIterator->current()) {
-                            if ($pathIterator->hasNext()) {
-                                $pathIterator->next();
-                            }
+            if (isset($parts[1])) {
+                $nestedFormPath .= '.data.' . $parts[1];
+            }
 
-                            $this->mapError($error, $child, $pathIterator, $type);
+            $nestedDataPath = $dataPath . '.' . $path;
 
-                            return;
-                        }
-                    }
-                }
+            if ($child->hasChildren()) {
+                $this->buildMapping($child, $mapping, $forms, $nestedNamePath, $nestedFormPath, $nestedDataPath);
+            } else {
+                $mapping['/^'.preg_quote($nestedFormPath, '/').'(?!\w)/'] = $child;
+                $mapping['/^'.preg_quote($nestedDataPath, '/').'(?!\w)/'] = $child;
             }
+
+            $forms[$nestedNamePath] = $child;
         }
 
-        $form->addError($error);
+        foreach ($form->getAttribute('error_mapping') as $nestedDataPath => $nestedNamePath)
+        {
+            $mapping['/^'.preg_quote($formPath . 'data.' . $nestedDataPath).'(?!\w)/'] = $namePath . $nestedNamePath;
+            $mapping['/^'.preg_quote($dataPath . '.' . $nestedDataPath).'(?!\w)/'] = $namePath . $nestedNamePath;
+        }
+    }
+
+    private function resolveMappingPlaceholders(array &$mapping, array $forms)
+    {
+        foreach ($mapping as $pattern => $form) {
+            if (is_string($form)) {
+                if (!isset($forms[$form])) {
+                    throw new FormException(sprintf('The child form with path "%s" does not exist', $form));
+                }
+
+                $mapping[$pattern] = $forms[$form];
+            }
+        }
     }
 }

+ 1 - 1
src/Symfony/Component/Form/RecursiveFormIterator.php

@@ -19,7 +19,7 @@ namespace Symfony\Component\Form;
  *
  * @author Bernhard Schussek <bernhard.schussek@symfony.com>
  */
-class RecursiveFormIterator extends \ArrayIterator implements \RecursiveIterator
+class VirtualFormIterator extends \ArrayIterator implements \RecursiveIterator
 {
     public function getChildren()
     {

+ 0 - 201
tests/Symfony/Tests/Component/Form/Type/FormTypeTest.php

@@ -330,207 +330,6 @@ class FormTest extends TestCase
         $form->bind(array());
     }
 
-    public function testAddErrorMapsFieldValidationErrorsOntoFields()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $error = new FormError('Message');
-
-        $field = $this->createMockField('firstName');
-        $field->expects($this->once())
-        ->method('addError')
-        ->with($this->equalTo($error));
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('fields[firstName].data');
-
-        $form->addError(new FormError('Message'), $path->getIterator());
-    }
-
-    public function testAddErrorMapsFieldValidationErrorsOntoFieldsWithinNestedForms()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $error = new FormError('Message');
-
-        $field = $this->createMockField('firstName');
-        $field->expects($this->once())
-        ->method('addError')
-        ->with($this->equalTo($error));
-
-        $form = $this->factory->create('form', 'author');
-        $innerGroup = $this->factory->create('form', 'names');
-        $innerGroup->add($field);
-        $form->add($innerGroup);
-
-        $path = new PropertyPath('fields[names].fields[firstName].data');
-
-        $form->addError(new FormError('Message'), $path->getIterator());
-    }
-
-    public function testAddErrorKeepsFieldValidationErrorsIfFieldNotFound()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $field = $this->createMockField('foo');
-        $field->expects($this->never())
-        ->method('addError');
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('fields[bar].data');
-
-        $form->addError(new FormError('Message'), $path->getIterator());
-
-        $this->assertEquals(array(new FormError('Message')), $form->getErrors());
-    }
-
-    public function testAddErrorKeepsFieldValidationErrorsIfFieldIsHidden()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $field = $this->createMockField('firstName');
-        $field->expects($this->any())
-        ->method('isHidden')
-        ->will($this->returnValue(true));
-        $field->expects($this->never())
-        ->method('addError');
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('fields[firstName].data');
-
-        $form->addError(new FormError('Message'), $path->getIterator());
-
-        $this->assertEquals(array(new FormError('Message')), $form->getErrors());
-    }
-
-    public function testAddErrorMapsDataValidationErrorsOntoFields()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $error = new DataError('Message');
-
-        // path is expected to point at "firstName"
-        $expectedPath = new PropertyPath('firstName');
-        $expectedPathIterator = $expectedPath->getIterator();
-
-        $field = $this->createMockField('firstName');
-        $field->expects($this->any())
-        ->method('getPropertyPath')
-        ->will($this->returnValue(new PropertyPath('firstName')));
-        $field->expects($this->once())
-        ->method('addError')
-        ->with($this->equalTo($error), $this->equalTo($expectedPathIterator));
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('firstName');
-
-        $form->addError($error, $path->getIterator());
-    }
-
-    public function testAddErrorKeepsDataValidationErrorsIfFieldNotFound()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $field = $this->createMockField('foo');
-        $field->expects($this->any())
-        ->method('getPropertyPath')
-        ->will($this->returnValue(new PropertyPath('foo')));
-        $field->expects($this->never())
-        ->method('addError');
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('bar');
-
-        $form->addError(new DataError('Message'), $path->getIterator());
-    }
-
-    public function testAddErrorKeepsDataValidationErrorsIfFieldIsHidden()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $field = $this->createMockField('firstName');
-        $field->expects($this->any())
-        ->method('isHidden')
-        ->will($this->returnValue(true));
-        $field->expects($this->any())
-        ->method('getPropertyPath')
-        ->will($this->returnValue(new PropertyPath('firstName')));
-        $field->expects($this->never())
-        ->method('addError');
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('firstName');
-
-        $form->addError(new DataError('Message'), $path->getIterator());
-    }
-
-    public function testAddErrorMapsDataValidationErrorsOntoNestedFields()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $error = new DataError('Message');
-
-        // path is expected to point at "street"
-        $expectedPath = new PropertyPath('address.street');
-        $expectedPathIterator = $expectedPath->getIterator();
-        $expectedPathIterator->next();
-
-        $field = $this->createMockField('address');
-        $field->expects($this->any())
-        ->method('getPropertyPath')
-        ->will($this->returnValue(new PropertyPath('address')));
-        $field->expects($this->once())
-        ->method('addError')
-        ->with($this->equalTo($error), $this->equalTo($expectedPathIterator));
-
-        $form = $this->factory->create('form', 'author');
-        $form->add($field);
-
-        $path = new PropertyPath('address.street');
-
-        $form->addError($error, $path->getIterator());
-    }
-
-    public function testAddErrorMapsErrorsOntoFieldsInVirtualGroups()
-    {
-        $this->markTestSkipped('Currently does not work');
-
-        $error = new DataError('Message');
-
-        // path is expected to point at "address"
-        $expectedPath = new PropertyPath('address');
-        $expectedPathIterator = $expectedPath->getIterator();
-
-        $field = $this->createMockField('address');
-        $field->expects($this->any())
-        ->method('getPropertyPath')
-        ->will($this->returnValue(new PropertyPath('address')));
-        $field->expects($this->once())
-        ->method('addError')
-        ->with($this->equalTo($error), $this->equalTo($expectedPathIterator));
-
-        $form = $this->factory->create('form', 'author');
-        $nestedForm = $this->factory->create('form', 'nested', array('virtual' => true));
-        $nestedForm->add($field);
-        $form->add($nestedForm);
-
-        $path = new PropertyPath('address');
-
-        $form->addError($error, $path->getIterator());
-    }
-
     public function testAddSetsFieldParent()
     {
         $this->markTestSkipped('Currently does not work');

+ 385 - 0
tests/Symfony/Tests/Component/Form/Validator/DelegatingValidatorTest.php

@@ -0,0 +1,385 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Tests\Component\Form\Validator;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\PropertyPath;
+use Symfony\Component\Form\Validator\DelegatingValidator;
+use Symfony\Component\Validator\ConstraintViolation;
+
+class DelegatingValidatorTest extends \PHPUnit_Framework_TestCase
+{
+    private $dispatcher;
+
+    private $builder;
+
+    private $delegate;
+
+    private $validator;
+
+    private $message;
+
+    private $params;
+
+    protected function setUp()
+    {
+        $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+        $this->delegate = $this->getMock('Symfony\Component\Validator\ValidatorInterface');
+        $this->validator = new DelegatingValidator($this->delegate);
+        $this->message = 'Message';
+        $this->params = array('foo' => 'bar');
+    }
+
+
+    protected function getConstraintViolation($propertyPath)
+    {
+        return new ConstraintViolation($this->message, $this->params, null, $propertyPath, null);
+    }
+
+    protected function getFormError()
+    {
+        return new FormError($this->message, $this->params);
+    }
+
+    protected function getBuilder($name, $propertyPath = null)
+    {
+        $builder = new FormBuilder($this->dispatcher);
+        $builder->setName($name);
+        $builder->setAttribute('property_path', new PropertyPath($propertyPath ?: $name));
+        $builder->setAttribute('error_mapping', array());
+
+        return $builder;
+    }
+
+    protected function getForm($name, $propertyPath = null)
+    {
+        return $this->getBuilder($name, $propertyPath)->getForm();
+    }
+
+    public function testFormErrorsOnForm()
+    {
+        $form = $this->getForm('author');
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('constrainedProp')
+            )));
+
+        $this->validator->validate($form);
+
+        $this->assertEquals(array($this->getFormError()), $form->getErrors());
+    }
+
+    public function testFormErrorsOnChild()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('firstName');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('children[firstName].constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $child->getErrors());
+    }
+
+    public function testFormErrorsOnChildLongPropertyPath()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('street', 'address.street');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('children[address].data.street.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $child->getErrors());
+    }
+
+    public function testFormErrorsOnGrandChild()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('address');
+        $grandChild = $this->getForm('street');
+
+        $parent->add($child);
+        $child->add($grandChild);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('children[address].children[street].constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertFalse($child->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
+    }
+
+    public function testFormErrorsOnParentIfNoChildFound()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('firstName');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('children[lastName].constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertEquals(array($this->getFormError()), $parent->getErrors());
+        $this->assertFalse($child->hasErrors());
+    }
+
+    public function testDataErrorsOnForm()
+    {
+        $form = $this->getForm('author');
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.constrainedProp')
+            )));
+
+        $this->validator->validate($form);
+
+        $this->assertEquals(array($this->getFormError()), $form->getErrors());
+    }
+
+    public function testDataErrorsOnChild()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('firstName');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.firstName.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $child->getErrors());
+    }
+
+    public function testDataErrorsOnChildLongPropertyPath()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('street', 'address.street');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.address.street.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $child->getErrors());
+    }
+
+    public function testDataErrorsOnGrandChild()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('address');
+        $grandChild = $this->getForm('street');
+
+        $parent->add($child);
+        $child->add($grandChild);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.address.street.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertFalse($child->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
+    }
+
+    public function testDataErrorsOnParentIfNoChildFound()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getForm('firstName');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.lastName.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertEquals(array($this->getFormError()), $parent->getErrors());
+        $this->assertFalse($child->hasErrors());
+    }
+
+    public function testMappedError()
+    {
+        $parent = $this->getBuilder('author')
+            ->setAttribute('error_mapping', array(
+                'passwordPlain' => 'password',
+            ))
+            ->getForm();
+        $child = $this->getForm('password');
+
+        $parent->add($child);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.passwordPlain.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $child->getErrors());
+    }
+
+    public function testMappedNestedError()
+    {
+        $parent = $this->getBuilder('author')
+            ->setAttribute('error_mapping', array(
+                'address.streetName' => 'address.street',
+            ))
+            ->getForm();
+        $child = $this->getForm('address');
+        $grandChild = $this->getForm('street');
+
+        $parent->add($child);
+        $child->add($grandChild);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.address.streetName.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertFalse($child->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
+    }
+
+    public function testNestedMappingUsingForm()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getBuilder('address')
+            ->setAttribute('error_mapping', array(
+                'streetName' => 'street',
+            ))
+            ->getForm();
+        $grandChild = $this->getForm('street');
+
+        $parent->add($child);
+        $child->add($grandChild);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('children[address].data.streetName.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertFalse($child->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
+    }
+
+    public function testNestedMappingUsingData()
+    {
+        $parent = $this->getForm('author');
+        $child = $this->getBuilder('address')
+            ->setAttribute('error_mapping', array(
+                'streetName' => 'street',
+            ))
+            ->getForm();
+        $grandChild = $this->getForm('street');
+
+        $parent->add($child);
+        $child->add($grandChild);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.address.streetName.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertFalse($child->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
+    }
+
+    public function testNestedMappingVirtualForm()
+    {
+        $parent = $this->getBuilder('author')
+            ->setAttribute('error_mapping', array(
+                'streetName' => 'street',
+            ))
+            ->getForm();
+        $child = $this->getBuilder('address')
+            ->setAttribute('virtual', true)
+            ->getForm();
+        $grandChild = $this->getForm('street');
+
+        $parent->add($child);
+        $child->add($grandChild);
+
+        $this->delegate->expects($this->once())
+            ->method('validate')
+            ->will($this->returnValue(array(
+                $this->getConstraintViolation('data.streetName.constrainedProp')
+            )));
+
+        $this->validator->validate($parent);
+
+        $this->assertFalse($parent->hasErrors());
+        $this->assertFalse($child->hasErrors());
+        $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
+    }
+}