[Form] Greatly improved the error mapping done in DelegatingValidationListener
This commit is contained in:
parent
8f7e2f602c
commit
306324ea0a
@ -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
|
||||
{
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
|
||||
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\Exception\ErrorMappingException;
|
||||
|
||||
/**
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class FormMapping
|
||||
{
|
||||
/**
|
||||
* @var FormInterface
|
||||
*/
|
||||
private $origin;
|
||||
|
||||
/**
|
||||
* @var FormInterface
|
||||
*/
|
||||
private $target;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $targetPath;
|
||||
|
||||
public function __construct(FormInterface $origin, $targetPath)
|
||||
{
|
||||
$this->origin = $origin;
|
||||
$this->targetPath = $targetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface
|
||||
*/
|
||||
public function getOrigin()
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface
|
||||
*
|
||||
* @throws ErrorMappingException
|
||||
*/
|
||||
public function getTarget()
|
||||
{
|
||||
// Lazy initialization to make sure that the constructor is cheap
|
||||
if (null === $this->target) {
|
||||
$childNames = explode('.', $this->targetPath);
|
||||
$target = $this->origin;
|
||||
|
||||
foreach ($childNames as $childName) {
|
||||
if (!$target->has($childName)) {
|
||||
throw new ErrorMappingException(sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName()));
|
||||
}
|
||||
$target = $target->get($childName);
|
||||
}
|
||||
|
||||
// Only set once successfully resolved
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
return $this->target;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
|
||||
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\Util\PropertyPath;
|
||||
|
||||
/**
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*/
|
||||
class RelativePath extends PropertyPath
|
||||
{
|
||||
/**
|
||||
* @var FormInterface
|
||||
*/
|
||||
private $root;
|
||||
|
||||
/**
|
||||
* @param FormInterface $root
|
||||
* @param string $propertyPath
|
||||
*/
|
||||
public function __construct(FormInterface $root, $propertyPath)
|
||||
{
|
||||
parent::__construct($propertyPath);
|
||||
|
||||
$this->root = $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface
|
||||
*/
|
||||
public function getRoot()
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
}
|
@ -0,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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
245
src/Symfony/Component/Form/Util/PropertyPathBuilder.php
Normal file
245
src/Symfony/Component/Form/Util/PropertyPathBuilder.php
Normal 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;
|
||||
}
|
||||
}
|
86
src/Symfony/Component/Form/Util/PropertyPathInterface.php
Normal file
86
src/Symfony/Component/Form/Util/PropertyPathInterface.php
Normal 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);
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
Reference in New Issue
Block a user