Skip to content

Commit

Permalink
[Form] Greatly improved the error mapping done in DelegatingValidatio…
Browse files Browse the repository at this point in the history
…nListener
  • Loading branch information
webmozart committed May 22, 2012
1 parent 8f7e2f6 commit 306324e
Show file tree
Hide file tree
Showing 17 changed files with 2,769 additions and 641 deletions.
16 changes: 16 additions & 0 deletions src/Symfony/Component/Form/Exception/ErrorMappingException.php
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Exception;

class ErrorMappingException extends FormException
{
}
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Validator\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvents;
Expand Down Expand Up @@ -129,14 +130,6 @@ public function validateForm(DataEvent $event)
$form = $event->getForm();

if ($form->isRoot()) {
$mapping = array();
$forms = array();

$this->buildFormPathMapping($form, $mapping);
$this->buildDataPathMapping($form, $mapping);
$this->buildNamePathMapping($form, $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
Expand All @@ -146,149 +139,16 @@ public function validateForm(DataEvent $event)
$form->getAttribute('validation_constraint'),
self::getFormValidationGroups($form)
);

if ($violations) {
foreach ($violations as $violation) {
$propertyPath = new PropertyPath($violation->getPropertyPath());
$template = $violation->getMessageTemplate();
$parameters = $violation->getMessageParameters();
$pluralization = $violation->getMessagePluralization();
$error = new FormError($template, $parameters, $pluralization);

$child = $form;
foreach ($propertyPath->getElements() as $element) {
$children = $child->getChildren();
if (!isset($children[$element])) {
$form->addError($error);
break;
}

$child = $children[$element];
}

$child->addError($error);
}
}
} elseif (count($violations = $this->validator->validate($form))) {
foreach ($violations as $violation) {
$propertyPath = $violation->getPropertyPath();
$template = $violation->getMessageTemplate();
$parameters = $violation->getMessageParameters();
$pluralization = $violation->getMessagePluralization();
$error = new FormError($template, $parameters, $pluralization);

foreach ($mapping as $mappedPath => $child) {
if (preg_match($mappedPath, $propertyPath)) {
$child->addError($error);
continue 2;
}
}

$form->addError($error);
}
}
}
}

private function buildFormPathMapping(FormInterface $form, array &$mapping, $formPath = 'children', $namePath = '')
{
foreach ($form->getAttribute('error_mapping') as $nestedDataPath => $nestedNamePath) {
$mapping['/^'.preg_quote($formPath.'.data.'.$nestedDataPath).'(?!\w)/'] = $namePath.'.'.$nestedNamePath;
}

$iterator = new VirtualFormAwareIterator($form->getChildren());
$iterator = new \RecursiveIteratorIterator($iterator);

foreach ($iterator as $child) {
$path = (string) $child->getAttribute('property_path');
$parts = explode('.', $path, 2);

$nestedNamePath = $namePath.'.'.$child->getName();

if ($child->hasChildren() || isset($parts[1])) {
$nestedFormPath = $formPath.'['.trim($parts[0], '[]').']';
} else {
$nestedFormPath = $formPath.'.data.'.$parts[0];
}

if (isset($parts[1])) {
$nestedFormPath .= '.data.'.$parts[1];
$violations = $this->validator->validate($form);
}

if ($child->hasChildren()) {
$this->buildFormPathMapping($child, $mapping, $nestedFormPath, $nestedNamePath);
}
if (count($violations) > 0) {
$mapper = new ViolationMapper();

$mapping['/^'.preg_quote($nestedFormPath, '/').'(?!\w)/'] = $child;
}
}

private function buildDataPathMapping(FormInterface $form, array &$mapping, $dataPath = 'data', $namePath = '')
{
foreach ($form->getAttribute('error_mapping') as $nestedDataPath => $nestedNamePath) {
$mapping['/^'.preg_quote($dataPath.'.'.$nestedDataPath).'(?!\w)/'] = $namePath.'.'.$nestedNamePath;
}

$iterator = new VirtualFormAwareIterator($form->getChildren());
$iterator = new \RecursiveIteratorIterator($iterator);

foreach ($iterator as $child) {
$path = (string) $child->getAttribute('property_path');

$nestedNamePath = $namePath.'.'.$child->getName();

if (0 === strpos($path, '[')) {
$nestedDataPaths = array($dataPath.$path);
} else {
$nestedDataPaths = array($dataPath.'.'.$path);
if ($child->hasChildren()) {
$nestedDataPaths[] = $dataPath.'['.$path.']';
}
}

if ($child->hasChildren()) {
// Needs when collection implements the Iterator
// or for array used the Valid validator.
if (is_array($child->getData()) || $child->getData() instanceof \Traversable) {
$this->buildDataPathMapping($child, $mapping, $dataPath, $nestedNamePath);
}

foreach ($nestedDataPaths as $nestedDataPath) {
$this->buildDataPathMapping($child, $mapping, $nestedDataPath, $nestedNamePath);
}
}

foreach ($nestedDataPaths as $nestedDataPath) {
$mapping['/^'.preg_quote($nestedDataPath, '/').'(?!\w)/'] = $child;
}
}
}

private function buildNamePathMapping(FormInterface $form, array &$forms, $namePath = '')
{
$iterator = new VirtualFormAwareIterator($form->getChildren());
$iterator = new \RecursiveIteratorIterator($iterator);

foreach ($iterator as $child) {
$nestedNamePath = $namePath.'.'.$child->getName();
$forms[$nestedNamePath] = $child;

if ($child->hasChildren()) {
$this->buildNamePathMapping($child, $forms, $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));
foreach ($violations as $violation) {
$mapper->mapViolation($violation, $form);
}

$mapping[$pattern] = $forms[$form];
}
}
}
Expand Down
@@ -0,0 +1,76 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;

use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Exception\ErrorMappingException;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormMapping
{
/**
* @var FormInterface
*/
private $origin;

/**
* @var FormInterface
*/
private $target;

/**
* @var string
*/
private $targetPath;

public function __construct(FormInterface $origin, $targetPath)
{
$this->origin = $origin;
$this->targetPath = $targetPath;
}

/**
* @return FormInterface
*/
public function getOrigin()
{
return $this->origin;
}

/**
* @return FormInterface
*
* @throws ErrorMappingException
*/
public function getTarget()
{
// Lazy initialization to make sure that the constructor is cheap
if (null === $this->target) {
$childNames = explode('.', $this->targetPath);
$target = $this->origin;

foreach ($childNames as $childName) {
if (!$target->has($childName)) {
throw new ErrorMappingException(sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName()));
}
$target = $target->get($childName);
}

// Only set once successfully resolved
$this->target = $target;
}

return $this->target;
}
}
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;

use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\PropertyPath;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RelativePath extends PropertyPath
{
/**
* @var FormInterface
*/
private $root;

/**
* @param FormInterface $root
* @param string $propertyPath
*/
public function __construct(FormInterface $root, $propertyPath)
{
parent::__construct($propertyPath);

$this->root = $root;
}

/**
* @return FormInterface
*/
public function getRoot()
{
return $this->root;
}
}

0 comments on commit 306324e

Please sign in to comment.