Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

[Form] Greatly improved the error mapping done in DelegatingValidatio…

…nListener
  • Loading branch information...
commit 66298c31e6ac7317617743f0b67f2e6bc5cd6d81 1 parent 6734c86
@webmozart webmozart authored
View
16 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
+{
+}
View
152 Extension/Validator/EventListener/DelegatingValidationListener.php
@@ -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;
@@ -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
@@ -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];
}
}
}
View
76 Extension/Validator/ViolationMapper/FormMapping.php
@@ -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;
+ }
+}
View
45 Extension/Validator/ViolationMapper/RelativePath.php
@@ -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;
+ }
+}
View
273 Extension/Validator/ViolationMapper/ViolationMapper.php
@@ -0,0 +1,273 @@
+<?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\VirtualFormAwareIterator;
+use Symfony\Component\Form\Util\PropertyPathIterator;
+use Symfony\Component\Form\Util\PropertyPathBuilder;
+use Symfony\Component\Form\Util\PropertyPathIteratorInterface;
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationMapper
+{
+ /**
+ * @var FormInterface
+ */
+ private $scope;
+
+ /**
+ * @var array
+ */
+ private $children;
+
+ /**
+ * @var array
+ */
+ private $rules = array();
+
+ /**
+ * Maps a constraint violation to a form in the form tree under
+ * the given form.
+ *
+ * @param ConstraintViolation $violation The violation to map.
+ * @param FormInterface $form The root form of the tree
+ * to map it to.
+ */
+ public function mapViolation(ConstraintViolation $violation, FormInterface $form)
+ {
+ $violationPath = new ViolationPath($violation->getPropertyPath());
+ $relativePath = $this->reconstructPath($violationPath, $form);
+ $match = false;
+
+ if (null !== $relativePath) {
+ // Set the scope to the root of the relative path
+ // This root will usually be $form. If the path contains
+ // an unmapped form though, the last unmapped form found
+ // will be the root of the path.
+ $this->setScope($relativePath->getRoot());
+ $it = new PropertyPathIterator($relativePath);
+
+ while (null !== ($child = $this->matchChild($it))) {
+ $this->setScope($child);
+ $it->next();
+ $match = true;
+ }
+ }
+
+ if (!$match) {
+ // If we could not map the error to anything more specific
+ // than the root element, map it to the innermost directly
+ // mapped form of the violation path
+ // e.g. "children[foo].children[bar].data.baz"
+ // Here the innermost directly mapped child is "bar"
+ $this->setScope($form);
+ $it = new ViolationPathIterator($violationPath);
+
+ while ($it->valid() && $it->mapsForm()) {
+ if (!$this->scope->has($it->current())) {
+ // Break if we find a reference to a non-existing child
+ break;
+ }
+
+ $this->setScope($this->scope->get($it->current()));
+ $it->next();
+ }
+ }
+
+ $template = $violation->getMessageTemplate();
+ $parameters = $violation->getMessageParameters();
+ $pluralization = $violation->getMessagePluralization();
+
+ $this->scope->addError(new FormError($template, $parameters, $pluralization));
+ }
+
+ /**
+ * Tries to match the beginning of the property path at the
+ * current position against the children of the scope.
+ *
+ * If a matching child is found, it is returned. Otherwise
+ * null is returned.
+ *
+ * @param PropertyPathIteratorInterface $it The iterator at its current position.
+ *
+ * @return null|FormInterface The found match or null.
+ */
+ private function matchChild(PropertyPathIteratorInterface $it)
+ {
+ // Remember at what property path underneath "data"
+ // we are looking. Check if there is a child with that
+ // path, otherwise increase path by one more piece
+ $chunk = '';
+ $foundChild = null;
+ $foundAtIndex = 0;
+
+ // Make the path longer until we find a matching child
+ while (true) {
+ if (!$it->valid()) {
+ return null;
+ }
+
+ if ($it->isIndex()) {
+ $chunk .= '[' . $it->current() . ']';
+ } else {
+ $chunk .= ('' === $chunk ? '' : '.') . $it->current();
+ }
+
+ // Test mapping rules as long as we have any
+ foreach ($this->rules as $path => $mapping) {
+ // Mapping rule matches completely, terminate.
+ if ($chunk === $path) {
+ /* @var FormMapping $mapping */
+ return $mapping->getTarget();
+ }
+
+ // Keep only rules that have $chunk as prefix
+ if (!$this->isPrefixPath($chunk, $path)) {
+ unset($this->rules[$path]);
+ }
+ }
+
+ // Test children unless we already found one
+ if (null === $foundChild) {
+ foreach ($this->children as $child) {
+ /* @var FormInterface $child */
+ $childPath = (string) $child->getPropertyPath();
+
+ // Child found, move scope inwards
+ if ($chunk === $childPath) {
+ $foundChild = $child;
+ $foundAtIndex = $it->key();
+ }
+ }
+ }
+
+ // Add element to the chunk
+ $it->next();
+
+ // If we reached the end of the path or if there are no
+ // more matching mapping rules, return the found child
+ if (null !== $foundChild && (!$it->valid() || count($this->rules) === 0)) {
+ // Reset index in case we tried to find mapping
+ // rules further down the path
+ $it->seek($foundAtIndex);
+
+ return $foundChild;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reconstructs a property path from a violation path and a form tree.
+ *
+ * @param ViolationPath $violationPath The violation path.
+ * @param FormInterface $origin The root form of the tree.
+ *
+ * @return RelativePath The reconstructed path.
+ */
+ private function reconstructPath(ViolationPath $violationPath, FormInterface $origin)
+ {
+ $propertyPathBuilder = new PropertyPathBuilder($violationPath);
+ $it = $violationPath->getIterator();
+ $scope = $origin;
+
+ // Remember the current index in the builder
+ $i = 0;
+
+ // Expand elements that map to a form (like "children[address]")
+ for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
+ if (!$scope->has($it->current())) {
+ // Scope relates to a form that does not exist
+ // Bail out
+ break;
+ }
+
+ // Process child form
+ $scope = $scope->get($it->current());
+
+ if ($scope->getAttribute('virtual')) {
+ // Form is virtual
+ // Cut the piece out of the property path and proceed
+ $propertyPathBuilder->remove($i);
+ } elseif (!$scope->getConfig()->getMapped()) {
+ // Form is not mapped
+ // Set the form as new origin and strip everything
+ // we have so far in the path
+ $origin = $scope;
+ $propertyPathBuilder->remove(0, $i + 1);
+ $i = 0;
+ } else {
+ /* @var \Symfony\Component\Form\Util\PropertyPathInterface $propertyPath */
+ $propertyPath = $scope->getPropertyPath();
+
+ if (null === $propertyPath) {
+ // Property path of a mapped form is null
+ // Should not happen, bail out
+ break;
+ } else {
+ $propertyPathBuilder->replace($i, 1, $propertyPath);
+ $i += $propertyPath->getLength();
+ }
+ }
+ }
+
+ $finalPath = $propertyPathBuilder->getPropertyPath();
+
+ return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
+ }
+
+ /**
+ * Sets the scope of the mapper to the given form.
+ *
+ * The scope is the currently found most specific form that
+ * an error should be mapped to. After setting the scope, the
+ * mapper will try to continue to find more specific matches in
+ * the children of scope. If it cannot, the error will be
+ * mapped to this scope.
+ *
+ * @param FormInterface $form The current scope.
+ */
+ private function setScope(FormInterface $form)
+ {
+ $this->scope = $form;
+ $this->children = new \RecursiveIteratorIterator(
+ new VirtualFormAwareIterator($form->getChildren())
+ );
+ foreach ($form->getAttribute('error_mapping') as $propertyPath => $childPath) {
+ $this->rules[$propertyPath] = new FormMapping($form, $childPath);
+ }
+ }
+
+ /**
+ * Tests whether $needle is a prefix path of $haystack.
+ *
+ * @param string $needle
+ * @param string $haystack
+ *
+ * @return Boolean
+ */
+ private function isPrefixPath($needle, $haystack)
+ {
+ $length = strlen($needle);
+ $prefix = substr($haystack, 0, $length);
+ $next = isset($haystack[$length]) ? $haystack[$length] : null;
+
+ return $prefix === $needle && ('[' === $next || '.' === $next);
+ }
+}
View
257 Extension/Validator/ViolationMapper/ViolationPath.php
@@ -0,0 +1,257 @@
+<?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\Util\PropertyPath;
+use Symfony\Component\Form\Util\PropertyPathIterator;
+use Symfony\Component\Form\Util\PropertyPathInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationPath implements \IteratorAggregate, PropertyPathInterface
+{
+ /**
+ * @var array
+ */
+ private $elements = array();
+
+ /**
+ * @var array
+ */
+ private $positions = array();
+
+ /**
+ * @var array
+ */
+ private $isIndex = array();
+
+ /**
+ * @var array
+ */
+ private $mapsForm = array();
+
+ /**
+ * @var string
+ */
+ private $string = '';
+
+ /**
+ * @var integer
+ */
+ private $length = 0;
+
+ /**
+ * Creates a new violation path from a string.
+ *
+ * @param string $violationPath The property path of a {@link ConstraintViolation}
+ * object.
+ */
+ public function __construct($violationPath)
+ {
+ $path = new PropertyPath($violationPath);
+ $pathElements = $path->getElements();
+ $pathPositions = $path->getPositions();
+ $elements = array();
+ $positions = array();
+ $isIndex = array();
+ $mapsForm = array();
+ $data = false;
+
+ for ($i = 0, $l = count($pathElements); $i < $l; ++$i) {
+ if (!$data) {
+ // The element "data" has not yet been passed
+ if ('children' === $pathElements[$i] && $path->isProperty($i)) {
+ // Skip element "children"
+ ++$i;
+
+ // Next element must exist and must be an index
+ // Otherwise not a valid path
+ if ($i >= $l || !$path->isIndex($i)) {
+ return;
+ }
+
+ $elements[] = $pathElements[$i];
+ $positions[] = $pathPositions[$i];
+ $isIndex[] = true;
+ $mapsForm[] = true;
+ } elseif ('data' === $pathElements[$i] && $path->isProperty($i)) {
+ // Skip element "data"
+ ++$i;
+
+ // End of path
+ if ($i >= $l) {
+ break;
+ }
+
+ $elements[] = $pathElements[$i];
+ $positions[] = $pathPositions[$i];
+ $isIndex[] = $path->isIndex($i);
+ $mapsForm[] = false;
+ $data = true;
+ } else {
+ // Neither "children" nor "data" property found
+ // Be nice and consider this the end of the path
+ break;
+ }
+ } else {
+ // Already after the "data" element
+ // Pick everything as is
+ $elements[] = $pathElements[$i];
+ $positions[] = $pathPositions[$i];
+ $isIndex[] = $path->isIndex($i);
+ $mapsForm[] = false;
+ }
+ }
+
+ $this->elements = $elements;
+ $this->positions = $positions;
+ $this->isIndex = $isIndex;
+ $this->mapsForm = $mapsForm;
+ $this->length = count($elements);
+ $this->string = $violationPath;
+
+ $this->resizeString();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->string;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPositions()
+ {
+ return $this->positions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLength()
+ {
+ return $this->length;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ if ($this->length <= 1) {
+ return null;
+ }
+
+ $parent = clone $this;
+
+ --$parent->length;
+ array_pop($parent->elements);
+ array_pop($parent->isIndex);
+ array_pop($parent->mapsForm);
+ array_pop($parent->positions);
+
+ $parent->resizeString();
+
+ return $parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElements()
+ {
+ return $this->elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElement($index)
+ {
+ return $this->elements[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isProperty($index)
+ {
+ return !$this->isIndex[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isIndex($index)
+ {
+ return $this->isIndex[$index];
+ }
+
+ /**
+ * Returns whether an element maps directly to a form.
+ *
+ * Consider the following violation path:
+ *
+ * <code>
+ * children[address].children[office].data.street
+ * </code>
+ *
+ * In this example, "address" and "office" map to forms, while
+ * "street does not.
+ *
+ * @param integer $index The element index.
+ *
+ * @return Boolean Whether the element maps to a form.
+ */
+ public function mapsForm($index)
+ {
+ return $this->mapsForm[$index];
+ }
+
+
+ /**
+ * Returns a new iterator for this path
+ *
+ * @return ViolationPathIterator
+ */
+ public function getIterator()
+ {
+ return new ViolationPathIterator($this);
+ }
+
+ /**
+ * Resizes the string representation to match the number of elements.
+ */
+ private function resizeString()
+ {
+ $lastIndex = $this->length - 1;
+
+ if ($lastIndex < 0) {
+ $this->string = '';
+ } else {
+ // +1 for the dot/opening bracket
+ $length = $this->positions[$lastIndex] + strlen($this->elements[$lastIndex]) + 1;
+
+ if ($this->isIndex[$lastIndex]) {
+ // +1 for the closing bracket
+ ++$length;
+ }
+
+ $this->string = substr($this->string, 0, $length);
+ }
+ }
+}
View
31 Extension/Validator/ViolationMapper/ViolationPathIterator.php
@@ -0,0 +1,31 @@
+<?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\Util\PropertyPathIterator;
+use Symfony\Component\Form\Util\PropertyPath;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationPathIterator extends PropertyPathIterator
+{
+ public function __construct(ViolationPath $violationPath)
+ {
+ parent::__construct($violationPath);
+ }
+
+ public function mapsForm()
+ {
+ return $this->path->mapsForm($this->key());
+ }
+}
View
447 Tests/Extension/Validator/EventListener/DelegatingValidationListenerTest.php
@@ -91,9 +91,10 @@ protected function getFormError()
protected function getBuilder($name = 'name', $propertyPath = null, $dataClass = null)
{
$builder = new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory);
- $builder->setAttribute('property_path', new PropertyPath($propertyPath ?: $name));
+ $builder->setPropertyPath(new PropertyPath($propertyPath ?: $name));
$builder->setAttribute('error_mapping', array());
$builder->setErrorBubbling(false);
+ $builder->setMapped(true);
return $builder;
}
@@ -131,51 +132,18 @@ public function testUseValidateValueWhenValidationConstraintExist()
$this->listener->validateForm(new DataEvent($form, null));
}
- public function testFormErrorsOnForm()
- {
- $form = $this->getForm();
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($form, null));
-
- $this->assertEquals(array($this->getFormError()), $form->getErrors());
- }
-
- public function testFormErrorsOnChild()
- {
- $parent = $this->getForm();
- $child = $this->getForm('firstName');
-
- $parent->add($child);
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('children.data.firstName')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertEquals(array($this->getFormError()), $child->getErrors());
- }
-
- public function testFormErrorsOnChildLongPropertyPath()
+ // More specific mapping tests can be found in ViolationMapperTest
+ public function testFormErrorMapping()
{
$parent = $this->getForm();
- $child = $this->getForm('street', 'address.street');
+ $child = $this->getForm('street');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
- $this->getConstraintViolation('children[address].data.street.constrainedProp')
+ $this->getConstraintViolation('children[street].data.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
@@ -184,115 +152,8 @@ public function testFormErrorsOnChildLongPropertyPath()
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
- public function testFormErrorsOnGrandChild()
- {
- $parent = $this->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('children[address].data.street')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testFormErrorsOnChildWithChildren()
- {
- $parent = $this->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('children[address].constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertEquals(array($this->getFormError()), $child->getErrors());
- $this->assertFalse($grandChild->hasErrors());
- }
-
- public function testFormErrorsOnParentIfNoChildFound()
- {
- $parent = $this->getForm();
- $child = $this->getForm('firstName');
-
- $parent->add($child);
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('children[lastName].constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertEquals(array($this->getFormError()), $parent->getErrors());
- $this->assertFalse($child->hasErrors());
- }
-
- public function testFormErrorsOnCollectionForm()
- {
- $parent = $this->getForm();
-
- for ($i = 0; $i < 2; $i++) {
- $child = $this->getForm((string)$i, '['.$i.']');
- $child->add($this->getForm('firstName'));
- $parent->add($child);
- }
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('children[0].data.firstName'),
- $this->getConstraintViolation('children[1].data.firstName'),
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
-
- foreach ($parent as $child) {
- $grandChild = $child->get('firstName');
-
- $this->assertFalse($child->hasErrors());
- $this->assertTrue($grandChild->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
- }
-
- public function testDataErrorsOnForm()
- {
- $form = $this->getForm();
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('data.constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($form, null));
-
- $this->assertEquals(array($this->getFormError()), $form->getErrors());
- }
-
- public function testDataErrorsOnChild()
+ // More specific mapping tests can be found in ViolationMapperTest
+ public function testDataErrorMapping()
{
$parent = $this->getForm();
$child = $this->getForm('firstName');
@@ -311,298 +172,6 @@ public function testDataErrorsOnChild()
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
- public function testDataErrorsOnChildLongPropertyPath()
- {
- $parent = $this->getForm();
- $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->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertEquals(array($this->getFormError()), $child->getErrors());
- }
-
- public function testDataErrorsOnChildWithChildren()
- {
- $parent = $this->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.constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertEquals(array($this->getFormError()), $child->getErrors());
- $this->assertFalse($grandChild->hasErrors());
- }
-
- public function testDataErrorsOnGrandChild()
- {
- $parent = $this->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.street.constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testDataErrorsOnGrandChild2()
- {
- $parent = $this->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('children[address].data.street.constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testDataErrorsOnGrandChild3()
- {
- $parent = $this->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].street.constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testDataErrorsOnParentIfNoChildFound()
- {
- $parent = $this->getForm();
- $child = $this->getForm('firstName');
-
- $parent->add($child);
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('data.lastName.constrainedProp')
- )));
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertEquals(array($this->getFormError()), $parent->getErrors());
- $this->assertFalse($child->hasErrors());
- }
-
- public function testDataErrorsOnCollectionForm()
- {
- $parent = $this->getForm();
- $child = $this->getForm('addresses');
-
- $parent->add($child);
-
- for ($i = 0; $i < 2; $i++) {
- $collection = $this->getForm((string)$i, '['.$i.']');
- $collection->add($this->getForm('street'));
-
- $child->add($collection);
- }
-
- $this->delegate->expects($this->once())
- ->method('validate')
- ->will($this->returnValue(array(
- $this->getConstraintViolation('data[0].street'),
- $this->getConstraintViolation('data.addresses[1].street')
- )));
-
- $child->setData(array());
-
- $this->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors(), '->hasErrors() returns false for parent form');
- $this->assertFalse($child->hasErrors(), '->hasErrors() returns false for child form');
-
- foreach ($child as $collection) {
- $grandChild = $collection->get('street');
-
- $this->assertFalse($collection->hasErrors());
- $this->assertTrue($grandChild->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
- }
-
- public function testMappedError()
- {
- $parent = $this->getBuilder()
- ->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->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertEquals(array($this->getFormError()), $child->getErrors());
- }
-
- public function testMappedNestedError()
- {
- $parent = $this->getBuilder()
- ->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->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testNestedMappingUsingForm()
- {
- $parent = $this->getForm();
- $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->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testNestedMappingUsingData()
- {
- $parent = $this->getForm();
- $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->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
- public function testNestedMappingVirtualForm()
- {
- $parent = $this->getBuilder()
- ->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->listener->validateForm(new DataEvent($parent, null));
-
- $this->assertFalse($parent->hasErrors());
- $this->assertFalse($child->hasErrors());
- $this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
- }
-
public function testValidateFormData()
{
$context = $this->getExecutionContext();
View
1,341 Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php
1,341 additions, 0 deletions not shown
View
133 Tests/Extension/Validator/ViolationMapper/ViolationPathTest.php
@@ -0,0 +1,133 @@
+<?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\Tests\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPath;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationPathTest extends \PHPUnit_Framework_TestCase
+{
+ public function providePaths()
+ {
+ return array(
+ array('children[address]', array(
+ array('address', true, true),
+ )),
+ array('children[address].children[street]', array(
+ array('address', true, true),
+ array('street', true, true),
+ )),
+ array('children[address][street]', array(
+ array('address', true, true),
+ ), 'children[address]'),
+ array('children[address].data', array(
+ array('address', true, true),
+ ), 'children[address]'),
+ array('children[address].data.street', array(
+ array('address', true, true),
+ array('street', false, false),
+ )),
+ array('children[address].data[street]', array(
+ array('address', true, true),
+ array('street', false, true),
+ )),
+ array('children[address].children[street].data.name', array(
+ array('address', true, true),
+ array('street', true, true),
+ array('name', false, false),
+ )),
+ array('children[address].children[street].data[name]', array(
+ array('address', true, true),
+ array('street', true, true),
+ array('name', false, true),
+ )),
+ array('data.address', array(
+ array('address', false, false),
+ )),
+ array('data[address]', array(
+ array('address', false, true),
+ )),
+ array('data.address.street', array(
+ array('address', false, false),
+ array('street', false, false),
+ )),
+ array('data[address].street', array(
+ array('address', false, true),
+ array('street', false, false),
+ )),
+ array('data.address[street]', array(
+ array('address', false, false),
+ array('street', false, true),
+ )),
+ array('data[address][street]', array(
+ array('address', false, true),
+ array('street', false, true),
+ )),
+ // A few invalid examples
+ array('data', array(), ''),
+ array('children', array(), ''),
+ array('children.address', array(), ''),
+ array('children.address[street]', array(), ''),
+ );
+ }
+
+ /**
+ * @dataProvider providePaths
+ */
+ public function testCreatePath($string, $entries, $slicedPath = null)
+ {
+ if (null === $slicedPath) {
+ $slicedPath = $string;
+ }
+
+ $path = new ViolationPath($string);
+
+ $this->assertSame($slicedPath, $path->__toString());
+ $this->assertSame(count($entries), count($path->getElements()));
+ $this->assertSame(count($entries), $path->getLength());
+
+ foreach ($entries as $index => $entry) {
+ $this->assertEquals($entry[0], $path->getElement($index));
+ $this->assertSame($entry[1], $path->mapsForm($index));
+ $this->assertSame($entry[2], $path->isIndex($index));
+ $this->assertSame(!$entry[2], $path->isProperty($index));
+ }
+ }
+
+ public function provideParents()
+ {
+ return array(
+ array('children[address]', null),
+ array('children[address].children[street]', 'children[address]'),
+ array('children[address].data.street', 'children[address]'),
+ array('children[address].data[street]', 'children[address]'),
+ array('data.address', null),
+ array('data.address.street', 'data.address'),
+ array('data.address[street]', 'data.address'),
+ array('data[address].street', 'data[address]'),
+ array('data[address][street]', 'data[address]'),
+ );
+ }
+
+ /**
+ * @dataProvider provideParents
+ */
+ public function testGetParent($violationPath, $parentPath)
+ {
+ $path = new ViolationPath($violationPath);
+ $parent = $parentPath === null ? null : new ViolationPath($parentPath);
+
+ $this->assertEquals($parent, $path->getParent());
+ }
+}
View
174 Tests/Util/PropertyPathBuilderTest.php
@@ -0,0 +1,174 @@
+<?php
+
+/*
+ * This file is new3 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\Tests\Util;
+
+use Symfony\Component\Form\Util\PropertyPath;
+use Symfony\Component\Form\Util\PropertyPathBuilder;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyPathBuilderTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var string
+ */
+ const PREFIX = 'old1[old2].old3[old4][old5].old6';
+
+ /**
+ * @var PropertyPathBuilder
+ */
+ private $builder;
+
+ protected function setUp()
+ {
+ $this->builder = new PropertyPathBuilder(new PropertyPath(self::PREFIX));
+ }
+
+ public function testCreateEmpty()
+ {
+ $builder = new PropertyPathBuilder();
+
+ $this->assertNull($builder->getPropertyPath());
+ }
+
+ public function testCreateCopyPath()
+ {
+ $this->assertEquals(new PropertyPath(self::PREFIX), $this->builder->getPropertyPath());
+ }
+
+ public function testAppendIndex()
+ {
+ $this->builder->appendIndex('new1');
+
+ $path = new PropertyPath(self::PREFIX . '[new1]');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testAppendProperty()
+ {
+ $this->builder->appendProperty('new1');
+
+ $path = new PropertyPath(self::PREFIX . '.new1');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testAppend()
+ {
+ $this->builder->append(new PropertyPath('new1[new2]'));
+
+ $path = new PropertyPath(self::PREFIX . '.new1[new2]');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testAppendWithOffset()
+ {
+ $this->builder->append(new PropertyPath('new1[new2].new3'), 1);
+
+ $path = new PropertyPath(self::PREFIX . '[new2].new3');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testAppendWithOffsetAndLength()
+ {
+ $this->builder->append(new PropertyPath('new1[new2].new3'), 1, 1);
+
+ $path = new PropertyPath(self::PREFIX . '[new2]');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceByIndex()
+ {
+ $this->builder->replaceByIndex(1, 1, 'new1');
+
+ $path = new PropertyPath('old1[new1].old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceByIndexWithLength()
+ {
+ $this->builder->replaceByIndex(0, 2, 'new1');
+
+ $path = new PropertyPath('[new1].old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceByProperty()
+ {
+ $this->builder->replaceByProperty(1, 1, 'new1');
+
+ $path = new PropertyPath('old1.new1.old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceByPropertyWithLength()
+ {
+ $this->builder->replaceByProperty(0, 2, 'new1');
+
+ $path = new PropertyPath('new1.old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplace()
+ {
+ $this->builder->replace(1, 1, new PropertyPath('new1[new2].new3'));
+
+ $path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceWithLengthGreaterOne()
+ {
+ $this->builder->replace(0, 2, new PropertyPath('new1[new2].new3'));
+
+ $path = new PropertyPath('new1[new2].new3.old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceSubstring()
+ {
+ $this->builder->replace(1, 1, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
+
+ $path = new PropertyPath('old1[new2].new3.new4.old3[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testReplaceSubstringWithLengthGreaterOne()
+ {
+ $this->builder->replace(1, 2, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
+
+ $path = new PropertyPath('old1[new2].new3.new4[old4][old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+
+ public function testRemove()
+ {
+ $this->builder->remove(3);
+
+ $path = new PropertyPath('old1[old2].old3[old5].old6');
+
+ $this->assertEquals($path, $this->builder->getPropertyPath());
+ }
+}
View
8 Tests/Util/PropertyPathTest.php
@@ -484,4 +484,12 @@ public function testGetParent_noParent()
$this->assertNull($propertyPath->getParent());
}
+
+ public function testCopyConstructor()
+ {
+ $propertyPath = new PropertyPath('grandpa.parent[child]');
+ $copy = new PropertyPath($propertyPath);
+
+ $this->assertEquals($propertyPath, $copy);
+ }
}
View
66 Util/PropertyPath.php
@@ -23,7 +23,7 @@
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
-class PropertyPath implements \IteratorAggregate
+class PropertyPath implements \IteratorAggregate, PropertyPathInterface
{
/**
* Character used for separating between plural and singular of an element.
@@ -71,12 +71,25 @@ class PropertyPath implements \IteratorAggregate
/**
* Constructs a property path from a string.
*
- * @param string $propertyPath The property path as string.
+ * @param PropertyPath|string $propertyPath The property path as string or instance.
*
+ * @throws UnexpectedTypeException If the given path is not a string.
* @throws InvalidPropertyPathException If the syntax of the property path is not valid.
*/
public function __construct($propertyPath)
{
+ // Can be used as copy constructor
+ if ($propertyPath instanceof PropertyPath) {
+ /* @var PropertyPath $propertyPath */
+ $this->elements = $propertyPath->elements;
+ $this->singulars = $propertyPath->singulars;
+ $this->length = $propertyPath->length;
+ $this->isIndex = $propertyPath->isIndex;
+ $this->string = $propertyPath->string;
+ $this->positions = $propertyPath->positions;
+
+ return;
+ }
if (!is_string($propertyPath)) {
throw new UnexpectedTypeException($propertyPath, 'string');
}
@@ -132,9 +145,7 @@ public function __construct($propertyPath)
}
/**
- * Returns the string representation of the property path
- *
- * @return string
+ * {@inheritdoc}
*/
public function __toString()
{
@@ -142,9 +153,15 @@ public function __toString()
}
/**
- * Returns the length of the property path.
- *
- * @return integer
+ * {@inheritdoc}
+ */
+ public function getPositions()
+ {
+ return $this->positions;
+ }
+
+ /**
+ * {@inheritdoc}
*/
public function getLength()
{
@@ -152,14 +169,7 @@ public function getLength()
}
/**
- * Returns the parent property path.
- *
- * The parent property path is the one that contains the same items as
- * this one except for the last one.
- *
- * If this property path only contains one item, null is returned.
- *
- * @return PropertyPath The parent path or null.
+ * {@inheritdoc}
*/
public function getParent()
{
@@ -182,7 +192,7 @@ public function getParent()
/**
* Returns a new iterator for this path
*
- * @return PropertyPathIterator
+ * @return PropertyPathIteratorInterface
*/
public function getIterator()
{
@@ -190,9 +200,7 @@ public function getIterator()
}
/**
- * Returns the elements of the property path as array
- *
- * @return array An array of property/index names
+ * {@inheritdoc}
*/
public function getElements()
{
@@ -200,11 +208,7 @@ public function getElements()
}
/**
- * Returns the element at the given index in the property path
- *
- * @param integer $index The index key
- *
- * @return string A property or index name
+ * {@inheritdoc}
*/
public function getElement($index)
{
@@ -212,11 +216,7 @@ public function getElement($index)
}
/**
- * Returns whether the element at the given index is a property
- *
- * @param integer $index The index in the property path
- *
- * @return Boolean Whether the element at this index is a property
+ * {@inheritdoc}
*/
public function isProperty($index)
{
@@ -224,11 +224,7 @@ public function isProperty($index)
}
/**
- * Returns whether the element at the given index is an array index
- *
- * @param integer $index The index in the property path
- *
- * @return Boolean Whether the element at this index is an array index
+ * {@inheritdoc}
*/
public function isIndex($index)
{
View
245 Util/PropertyPathBuilder.php
@@ -0,0 +1,245 @@
+<?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\Util;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyPathBuilder
+{
+ /**
+ * @var array
+ */
+ private $elements = array();
+
+ /**
+ * @var array
+ */
+ private $isIndex = array();
+
+ /**
+ * Creates a new property path builder.
+ *
+ * @param null|PropertyPathInterface $path The path to initially store
+ * in the builder. Optional.
+ */
+ public function __construct(PropertyPathInterface $path = null)
+ {
+ if (null !== $path) {
+ $this->append($path);
+ }
+ }
+
+ /**
+ * Appends a (sub-) path to the current path.
+ *
+ * @param PropertyPathInterface $path The path to append.
+ * @param integer $offset The offset where the appended piece
+ * starts in $path.
+ * @param integer $length The length of the appended piece.
+ */
+ public function append(PropertyPathInterface $path, $offset = 0, $length = 0)
+ {
+ if (0 === $length) {
+ $end = $path->getLength();
+ } else {
+ $end = $offset + $length;
+ }
+
+ for (; $offset < $end; ++$offset) {
+ $this->elements[] = $path->getElement($offset);
+ $this->isIndex[] = $path->isIndex($offset);
+ }
+ }
+
+ /**
+ * Appends an index element to the current path.
+ *
+ * @param string $name The name of the appended index.
+ */
+ public function appendIndex($name)
+ {
+ $this->elements[] = $name;
+ $this->isIndex[] = true;
+ }
+
+ /**
+ * Appends a property element to the current path.
+ *
+ * @param string $name The name of the appended property.
+ */
+ public function appendProperty($name)
+ {
+ $this->elements[] = $name;
+ $this->isIndex[] = false;
+ }
+
+ /**
+ * Removes elements from the current path.
+ *
+ * @param integer $offset The offset at which to remove.
+ * @param integer $length The length of the removed piece.
+ */
+ public function remove($offset, $length = 1)
+ {
+ $this->resize($offset, $length, 0);
+ }
+
+ /**
+ * Replaces a sub-path by a different (sub-) path.
+ *
+ * @param integer $offset The offset at which to replace.
+ * @param integer $length The length of the piece to replace.
+ * @param PropertyPathInterface $path The path to insert.
+ * @param integer $pathOffset The offset where the inserted piece
+ * starts in $path.
+ * @param integer $pathLength The length of the inserted piece.
+ */
+ public function replace($offset, $length, PropertyPathInterface $path, $pathOffset = 0, $pathLength = 0)
+ {
+ if (0 === $pathLength) {
+ $pathLength = $path->getLength() - $pathOffset;
+ }
+
+ $this->resize($offset, $length, $pathLength);
+
+ for ($i = 0; $i < $pathLength; ++$i) {
+ $this->elements[$offset + $i] = $path->getElement($pathOffset + $i);
+ $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i);
+ }
+ }
+
+ /**
+ * Replaces a sub-path by a single index element.
+ *
+ * @param integer $offset The offset at which to replace.
+ * @param integer $length The length of the piece to replace.
+ * @param string $name The inserted index name.
+ */
+ public function replaceByIndex($offset, $length, $name)
+ {
+ $this->resize($offset, $length, 1);
+
+ $this->elements[$offset] = $name;
+ $this->isIndex[$offset] = true;
+ }
+
+ /**
+ * Replaces a sub-path by a single property element.
+ *
+ * @param integer $offset The offset at which to replace.
+ * @param integer $length The length of the piece to replace.
+ * @param string $name The inserted property name.
+ */
+ public function replaceByProperty($offset, $length, $name)
+ {
+ $this->resize($offset, $length, 1);
+
+ $this->elements[$offset] = $name;
+ $this->isIndex[$offset] = false;
+ }
+
+ /**
+ * Resizes the path so that a chunk of length $cutLength is
+ * removed at $offset and another chunk of length $insertionLength
+ * is inserted.
+ *
+ * @param integer $offset The offset where a chunk should be removed.
+ * @param $cutLength
+ * @param $insertionLength
+ * @return mixed
+ */
+ private function resize($offset, $cutLength, $insertionLength)
+ {
+ // Nothing else to do in this case
+ if ($insertionLength === $cutLength) {
+ return;
+ }
+
+ $length = count($this->elements);
+
+ if ($cutLength > $insertionLength) {
+ // More elements should be removed than inserted
+ $diff = $cutLength - $insertionLength;
+ $newLength = $length - $diff;
+
+ // Shift elements to the left (left-to-right until the new end)
+ // Max allowed offset to be shifted is such that
+ // $offset + $diff < $length (otherwise invalid index access)
+ // i.e. $offset < $length - $diff = $newLength
+ for ($i = $offset; $i < $newLength; ++$i) {
+ $this->elements[$i] = $this->elements[$i + $diff];
+ $this->isIndex[$i] = $this->isIndex[$i + $diff];
+ }
+
+ // All remaining elements should be removed
+ for (; $i < $length; ++$i) {
+ unset($this->elements[$i]);
+ unset($this->isIndex[$i]);
+ }
+ } else {
+ $diff = $insertionLength - $cutLength;
+ $newLength = $length + $diff;
+ $indexAfterInsertion = $offset + $insertionLength;
+
+ // Shift old elements to the right to make up space for the
+ // inserted elements. This needs to be done left-to-right in
+ // order to preserve an ascending array index order
+ for ($i = $length; $i < $newLength; ++$i) {
+ $this->elements[$i] = $this->elements[$i - $diff];
+ $this->isIndex[$i] = $this->isIndex[$i - $diff];
+ }
+
+ // Shift remaining elements to the right. Do this right-to-left
+ // so we don't overwrite elements before copying them
+ // The last written index is the immediate index after the inserted
+ // string, because the indices before that will be overwritten
+ // anyway.
+ for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
+ $this->elements[$i] = $this->elements[$i - $diff];
+ $this->isIndex[$i] = $this->isIndex[$i - $diff];
+ }
+ }
+ }
+
+ /**
+ * Returns the length of the current path.
+ *
+ * @return integer The path length.
+ */
+ public function getLength()
+ {
+ return count($this->elements);
+ }
+
+ /**
+ * Returns the current property path.
+ *
+ * @return PropertyPathInterface The constructed property path.
+ */
+ public function getPropertyPath()
+ {
+ $string = null;
+
+ foreach ($this->elements as $offset => $element) {
+ if ($this->isIndex[$offset]) {
+ $element = '[' . $element . ']';
+ } elseif (null !== $string) {
+ $string .= '.';
+ }
+
+ $string .= $element;
+ }
+
+ return null !== $string ? new PropertyPath($string) : null;
+ }
+}
View
86 Util/PropertyPathInterface.php
@@ -0,0 +1,86 @@
+<?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\Util;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface PropertyPathInterface extends \Traversable
+{
+ /**
+ * Returns the string representation of the property path
+ *
+ * @return string The path as string.
+ */
+ function __toString();
+
+ /**
+ * Returns the positions at which the elements of the path
+ * start in the string.
+ *
+ * @return array The string offsets of the elements.
+ */
+ function getPositions();
+
+ /**
+ * Returns the length of the property path.
+ *
+ * @return integer The path length.
+ */
+ function getLength();
+
+ /**
+ * Returns the parent property path.
+ *
+ * The parent property path is the one that contains the same items as
+ * this one except for the last one.
+ *
+ * If this property path only contains one item, null is returned.
+ *
+ * @return PropertyPath The parent path or null.
+ */
+ function getParent();
+
+ /**
+ * Returns the elements of the property path as array
+ *
+ * @return array An array of property/index names
<