From 4f5b44bd3bd490309eb2ba7b44df4769816ba729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Sat, 3 Aug 2013 19:26:54 +0200 Subject: twig implementation --- .../Validator/ViolationMapper/MappingRule.php | 106 ++++++++ .../Validator/ViolationMapper/RelativePath.php | 45 ++++ .../Validator/ViolationMapper/ViolationMapper.php | 299 +++++++++++++++++++++ .../ViolationMapper/ViolationMapperInterface.php | 33 +++ .../Validator/ViolationMapper/ViolationPath.php | 250 +++++++++++++++++ .../ViolationMapper/ViolationPathIterator.php | 30 +++ 6 files changed, 763 insertions(+) create mode 100644 vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/MappingRule.php create mode 100644 vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php create mode 100644 vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php create mode 100644 vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php create mode 100644 vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php create mode 100644 vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php (limited to 'vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper') diff --git a/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/MappingRule.php b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/MappingRule.php new file mode 100644 index 00000000..7b96efb4 --- /dev/null +++ b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/MappingRule.php @@ -0,0 +1,106 @@ + + * + * 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 + */ +class MappingRule +{ + /** + * @var FormInterface + */ + private $origin; + + /** + * @var string + */ + private $propertyPath; + + /** + * @var string + */ + private $targetPath; + + public function __construct(FormInterface $origin, $propertyPath, $targetPath) + { + $this->origin = $origin; + $this->propertyPath = $propertyPath; + $this->targetPath = $targetPath; + } + + /** + * @return FormInterface + */ + public function getOrigin() + { + return $this->origin; + } + + /** + * Matches a property path against the rule path. + * + * If the rule matches, the form mapped by the rule is returned. + * Otherwise this method returns false. + * + * @param string $propertyPath The property path to match against the rule. + * + * @return null|FormInterface The mapped form or null. + */ + public function match($propertyPath) + { + if ($propertyPath === (string) $this->propertyPath) { + return $this->getTarget(); + } + + return null; + } + + /** + * Matches a property path against a prefix of the rule path. + * + * @param string $propertyPath The property path to match against the rule. + * + * @return Boolean Whether the property path is a prefix of the rule or not. + */ + public function isPrefix($propertyPath) + { + $length = strlen($propertyPath); + $prefix = substr($this->propertyPath, 0, $length); + $next = isset($this->propertyPath[$length]) ? $this->propertyPath[$length] : null; + + return $prefix === $propertyPath && ('[' === $next || '.' === $next); + } + + /** + * @return FormInterface + * + * @throws ErrorMappingException + */ + public function getTarget() + { + $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); + } + + return $target; + } +} diff --git a/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php new file mode 100644 index 00000000..ef5c9fad --- /dev/null +++ b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php @@ -0,0 +1,45 @@ + + * + * 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\PropertyAccess\PropertyPath; + +/** + * @author Bernhard Schussek + */ +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; + } +} diff --git a/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php new file mode 100644 index 00000000..8a7636c7 --- /dev/null +++ b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -0,0 +1,299 @@ + + * + * 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\InheritDataAwareIterator; +use Symfony\Component\PropertyAccess\PropertyPathIterator; +use Symfony\Component\PropertyAccess\PropertyPathBuilder; +use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface; +use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator; +use Symfony\Component\Form\FormError; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * @author Bernhard Schussek + */ +class ViolationMapper implements ViolationMapperInterface +{ + /** + * @var Boolean + */ + private $allowNonSynchronized; + + /** + * {@inheritdoc} + */ + public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false) + { + $this->allowNonSynchronized = $allowNonSynchronized; + + // 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. + $scope = null; + + $violationPath = null; + $relativePath = null; + $match = false; + + // Don't create a ViolationPath instance for empty property paths + if (strlen($violation->getPropertyPath()) > 0) { + $violationPath = new ViolationPath($violation->getPropertyPath()); + $relativePath = $this->reconstructPath($violationPath, $form); + } + + // This case happens if the violation path is empty and thus + // the violation should be mapped to the root form + if (null === $violationPath) { + $scope = $form; + } + + // In general, mapping happens from the root form to the leaf forms + // First, the rules of the root form are applied to determine + // the subsequent descendant. The rules of this descendant are then + // applied to find the next and so on, until we have found the + // most specific form that matches the violation. + + // If any of the forms found in this process is not synchronized, + // mapping is aborted. Non-synchronized forms could not reverse + // transform the value entered by the user, thus any further violations + // caused by the (invalid) reverse transformed value should be + // ignored. + + 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. + $scope = $relativePath->getRoot(); + $it = new PropertyPathIterator($relativePath); + + while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) { + $scope = $child; + $it->next(); + $match = true; + } + } + + // This case happens if an error happened in the data under a + // form inheriting its parent data that does not match any of the + // children of that form. + if (null !== $violationPath && !$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" + + $scope = $form; + $it = new ViolationPathIterator($violationPath); + + // Note: acceptsErrors() will always return true for forms inheriting + // their parent data, because these forms can never be non-synchronized + // (they don't do any data transformation on their own) + while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) { + if (!$scope->has($it->current())) { + // Break if we find a reference to a non-existing child + break; + } + + $scope = $scope->get($it->current()); + $it->next(); + } + } + + // Follow dot rules until we have the final target + $mapping = $scope->getConfig()->getOption('error_mapping'); + + while ($this->acceptsErrors($scope) && isset($mapping['.'])) { + $dotRule = new MappingRule($scope, '.', $mapping['.']); + $scope = $dotRule->getTarget(); + $mapping = $scope->getConfig()->getOption('error_mapping'); + } + + // Only add the error if the form is synchronized + if ($this->acceptsErrors($scope)) { + $scope->addError(new FormError( + $violation->getMessage(), + $violation->getMessageTemplate(), + $violation->getMessageParameters(), + $violation->getMessagePluralization() + )); + } + } + + /** + * 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 FormInterface $form The form to search. + * @param PropertyPathIteratorInterface $it The iterator at its current position. + * + * @return null|FormInterface The found match or null. + */ + private function matchChild(FormInterface $form, 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; + + // Construct mapping rules for the given form + $rules = array(); + + foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) { + // Dot rules are considered at the very end + if ('.' !== $propertyPath) { + $rules[] = new MappingRule($form, $propertyPath, $targetPath); + } + } + + // Skip forms inheriting their parent data when iterating the children + $childIterator = new \RecursiveIteratorIterator( + new InheritDataAwareIterator($form->all()) + ); + + // 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 ($rules as $key => $rule) { + /* @var MappingRule $rule */ + + // Mapping rule matches completely, terminate. + if (null !== ($form = $rule->match($chunk))) { + return $form; + } + + // Keep only rules that have $chunk as prefix + if (!$rule->isPrefix($chunk)) { + unset($rules[$key]); + } + } + + // Test children unless we already found one + if (null === $foundChild) { + foreach ($childIterator as $child) { + /* @var FormInterface $child */ + $childPath = (string) $child->getPropertyPath(); + + // Child found, mark as return value + 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($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->getConfig()->getInheritData()) { + // Form inherits its parent data + // 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\PropertyAccess\PropertyPathInterface $propertyPath */ + $propertyPath = $scope->getPropertyPath(); + + if (null === $propertyPath) { + // Property path of a mapped form is null + // Should not happen, bail out + break; + } + + $propertyPathBuilder->replace($i, 1, $propertyPath); + $i += $propertyPath->getLength(); + } + } + + $finalPath = $propertyPathBuilder->getPropertyPath(); + + return null !== $finalPath ? new RelativePath($origin, $finalPath) : null; + } + + /** + * @param FormInterface $form + * + * @return Boolean + */ + private function acceptsErrors(FormInterface $form) + { + return $this->allowNonSynchronized || $form->isSynchronized(); + } +} diff --git a/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php new file mode 100644 index 00000000..eb8907f1 --- /dev/null +++ b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php @@ -0,0 +1,33 @@ + + * + * 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\Validator\ConstraintViolation; + +/** + * @author Bernhard Schussek + */ +interface ViolationMapperInterface +{ + /** + * 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. + * @param Boolean $allowNonSynchronized Whether to allow + * mapping to non-synchronized forms. + */ + public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false); +} diff --git a/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php new file mode 100644 index 00000000..06d09195 --- /dev/null +++ b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php @@ -0,0 +1,250 @@ + + * + * 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\Exception\OutOfBoundsException; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * @author Bernhard Schussek + */ +class ViolationPath implements \IteratorAggregate, PropertyPathInterface +{ + /** + * @var array + */ + private $elements = array(); + + /** + * @var array + */ + private $isIndex = array(); + + /** + * @var array + */ + private $mapsForm = array(); + + /** + * @var string + */ + private $pathAsString = ''; + + /** + * @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); + $elements = $path->getElements(); + $data = false; + + for ($i = 0, $l = count($elements); $i < $l; ++$i) { + if (!$data) { + // The element "data" has not yet been passed + if ('children' === $elements[$i] && $path->isProperty($i)) { + // Skip element "children" + ++$i; + + // Next element must exist and must be an index + // Otherwise consider this the end of the path + if ($i >= $l || !$path->isIndex($i)) { + break; + } + + $this->elements[] = $elements[$i]; + $this->isIndex[] = true; + $this->mapsForm[] = true; + } elseif ('data' === $elements[$i] && $path->isProperty($i)) { + // Skip element "data" + ++$i; + + // End of path + if ($i >= $l) { + break; + } + + $this->elements[] = $elements[$i]; + $this->isIndex[] = $path->isIndex($i); + $this->mapsForm[] = false; + $data = true; + } else { + // Neither "children" nor "data" property found + // Consider this the end of the path + break; + } + } else { + // Already after the "data" element + // Pick everything as is + $this->elements[] = $elements[$i]; + $this->isIndex[] = $path->isIndex($i); + $this->mapsForm[] = false; + } + } + + $this->length = count($this->elements); + + $this->buildString(); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->pathAsString; + } + + /** + * {@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); + + $parent->buildString(); + + return $parent; + } + + /** + * {@inheritdoc} + */ + public function getElements() + { + return $this->elements; + } + + /** + * {@inheritdoc} + */ + public function getElement($index) + { + if (!isset($this->elements[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index)); + } + + return $this->elements[$index]; + } + + /** + * {@inheritdoc} + */ + public function isProperty($index) + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index)); + } + + return !$this->isIndex[$index]; + } + + /** + * {@inheritdoc} + */ + public function isIndex($index) + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index)); + } + + return $this->isIndex[$index]; + } + + /** + * Returns whether an element maps directly to a form. + * + * Consider the following violation path: + * + * + * children[address].children[office].data.street + * + * + * 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. + * + * @throws OutOfBoundsException If the offset is invalid. + */ + public function mapsForm($index) + { + if (!isset($this->mapsForm[$index])) { + throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index)); + } + + return $this->mapsForm[$index]; + } + + /** + * Returns a new iterator for this path + * + * @return ViolationPathIterator + */ + public function getIterator() + { + return new ViolationPathIterator($this); + } + + /** + * Builds the string representation from the elements. + */ + private function buildString() + { + $this->pathAsString = ''; + $data = false; + + foreach ($this->elements as $index => $element) { + if ($this->mapsForm[$index]) { + $this->pathAsString .= ".children[$element]"; + } elseif (!$data) { + $this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element"); + $data = true; + } else { + $this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element"; + } + } + + if ('' !== $this->pathAsString) { + // remove leading dot + $this->pathAsString = substr($this->pathAsString, 1); + } + } +} diff --git a/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php new file mode 100644 index 00000000..50baa453 --- /dev/null +++ b/vendor/symfony/form/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php @@ -0,0 +1,30 @@ + + * + * 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\PropertyAccess\PropertyPathIterator; + +/** + * @author Bernhard Schussek + */ +class ViolationPathIterator extends PropertyPathIterator +{ + public function __construct(ViolationPath $violationPath) + { + parent::__construct($violationPath); + } + + public function mapsForm() + { + return $this->path->mapsForm($this->key()); + } +} -- cgit v1.2.3