[Form] Greatly improved the error mapping done in DelegatingValidationListener

This commit is contained in:
Bernhard Schussek 2012-05-19 23:16:21 +02:00
parent 8f7e2f602c
commit 306324ea0a
17 changed files with 2770 additions and 642 deletions

View File

@ -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 File

@ -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 @@ class DelegatingValidationListener implements EventSubscriberInterface
$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,150 +139,17 @@ class DelegatingValidationListener implements EventSubscriberInterface
$form->getAttribute('validation_constraint'),
self::getFormValidationGroups($form)
);
} else {
$violations = $this->validator->validate($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);
if (count($violations) > 0) {
$mapper = new ViolationMapper();
$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);
$mapper->mapViolation($violation, $form);
}
}
}
}
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];
}
if ($child->hasChildren()) {
$this->buildFormPathMapping($child, $mapping, $nestedFormPath, $nestedNamePath);
}
$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));
}
$mapping[$pattern] = $forms[$form];
}
}
}
}

View File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -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 File

@ -91,9 +91,10 @@ class DelegatingValidationListenerTest extends \PHPUnit_Framework_TestCase
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,32 +132,18 @@ class DelegatingValidationListenerTest extends \PHPUnit_Framework_TestCase
$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()
// More specific mapping tests can be found in ViolationMapperTest
public function testFormErrorMapping()
{
$parent = $this->getForm();
$child = $this->getForm('firstName');
$child = $this->getForm('street');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children.data.firstName')
$this->getConstraintViolation('children[street].data.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
@ -165,134 +152,8 @@ class DelegatingValidationListenerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
public function testFormErrorsOnChildLongPropertyPath()
{
$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('children[address].data.street.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$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 @@ class DelegatingValidationListenerTest extends \PHPUnit_Framework_TestCase
$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 File

@ -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 File

@ -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 File

@ -484,4 +484,12 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertNull($propertyPath->getParent());
}
public function testCopyConstructor()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$copy = new PropertyPath($propertyPath);
$this->assertEquals($propertyPath, $copy);
}
}

View File

@ -23,7 +23,7 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException;
*
* @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 @@ class PropertyPath implements \IteratorAggregate
}
/**
* Returns the string representation of the property path
*
* @return string
* {@inheritdoc}
*/
public function __toString()
{
@ -142,9 +153,15 @@ class PropertyPath implements \IteratorAggregate
}
/**
* Returns the length of the property path.
*
* @return integer
* {@inheritdoc}
*/
public function getPositions()
{
return $this->positions;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
@ -152,14 +169,7 @@ class PropertyPath implements \IteratorAggregate
}
/**
* 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 @@ class PropertyPath implements \IteratorAggregate
/**
* Returns a new iterator for this path
*
* @return PropertyPathIterator
* @return PropertyPathIteratorInterface
*/
public function getIterator()
{
@ -190,9 +200,7 @@ class PropertyPath implements \IteratorAggregate
}
/**
* 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 @@ class PropertyPath implements \IteratorAggregate
}
/**
* 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 @@ class PropertyPath implements \IteratorAggregate
}
/**
* 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 @@ class PropertyPath implements \IteratorAggregate
}
/**
* 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 File

@ -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 File

@ -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
*/
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
*/
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
*/
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
*/
function isIndex($index);
}

View File

@ -17,7 +17,7 @@ namespace Symfony\Component\Form\Util;
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class PropertyPathIterator extends \ArrayIterator
class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface
{
/**
* The traversed property path
@ -28,9 +28,9 @@ class PropertyPathIterator extends \ArrayIterator
/**
* Constructor.
*
* @param PropertyPath $path The property path to traverse
* @param PropertyPathInterface $path The property path to traverse
*/
public function __construct(PropertyPath $path)
public function __construct(PropertyPathInterface $path)
{
parent::__construct($path->getElements());
@ -38,20 +38,7 @@ class PropertyPathIterator extends \ArrayIterator
}
/**
* Returns whether next() can be called without making the iterator invalid
*
* @return Boolean
*/
public function hasNext()
{
return $this->offsetExists($this->key() + 1);
}
/**
* Returns whether the current element in the property path is an array
* index
*
* @return Boolean
* {@inheritdoc}
*/
public function isIndex()
{
@ -59,10 +46,7 @@ class PropertyPathIterator extends \ArrayIterator
}
/**
* Returns whether the current element in the property path is a property
* names
*
* @return Boolean
* {@inheritdoc}
*/
public function isProperty()
{

View File

@ -0,0 +1,34 @@
<?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 PropertyPathIteratorInterface extends \Iterator, \SeekableIterator
{
/**
* Returns whether the current element in the property path is an array
* index.
*
* @return Boolean
*/
function isIndex();
/**
* Returns whether the current element in the property path is a property
* names.
*
* @return Boolean
*/
function isProperty();
}