4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Form\Extension\Validator\ViolationMapper
;
14 use Symfony\Component\Form\FormInterface
;
15 use Symfony\Component\Form\Util\InheritDataAwareIterator
;
16 use Symfony\Component\PropertyAccess\PropertyPathIterator
;
17 use Symfony\Component\PropertyAccess\PropertyPathBuilder
;
18 use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface
;
19 use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator
;
20 use Symfony\Component\Form\FormError
;
21 use Symfony\Component\Validator\ConstraintViolation
;
24 * @author Bernhard Schussek <bschussek@gmail.com>
26 class ViolationMapper
implements ViolationMapperInterface
31 private $allowNonSynchronized;
36 public function mapViolation(ConstraintViolation
$violation, FormInterface
$form, $allowNonSynchronized = false)
38 $this->allowNonSynchronized
= $allowNonSynchronized;
40 // The scope is the currently found most specific form that
41 // an error should be mapped to. After setting the scope, the
42 // mapper will try to continue to find more specific matches in
43 // the children of scope. If it cannot, the error will be
44 // mapped to this scope.
47 $violationPath = null;
51 // Don't create a ViolationPath instance for empty property paths
52 if (strlen($violation->getPropertyPath()) > 0) {
53 $violationPath = new ViolationPath($violation->getPropertyPath());
54 $relativePath = $this->reconstructPath($violationPath, $form);
57 // This case happens if the violation path is empty and thus
58 // the violation should be mapped to the root form
59 if (null === $violationPath) {
63 // In general, mapping happens from the root form to the leaf forms
64 // First, the rules of the root form are applied to determine
65 // the subsequent descendant. The rules of this descendant are then
66 // applied to find the next and so on, until we have found the
67 // most specific form that matches the violation.
69 // If any of the forms found in this process is not synchronized,
70 // mapping is aborted. Non-synchronized forms could not reverse
71 // transform the value entered by the user, thus any further violations
72 // caused by the (invalid) reverse transformed value should be
75 if (null !== $relativePath) {
76 // Set the scope to the root of the relative path
77 // This root will usually be $form. If the path contains
78 // an unmapped form though, the last unmapped form found
79 // will be the root of the path.
80 $scope = $relativePath->getRoot();
81 $it = new PropertyPathIterator($relativePath);
83 while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
90 // This case happens if an error happened in the data under a
91 // form inheriting its parent data that does not match any of the
92 // children of that form.
93 if (null !== $violationPath && !$match) {
94 // If we could not map the error to anything more specific
95 // than the root element, map it to the innermost directly
96 // mapped form of the violation path
97 // e.g. "children[foo].children[bar].data.baz"
98 // Here the innermost directly mapped child is "bar"
101 $it = new ViolationPathIterator($violationPath);
103 // Note: acceptsErrors() will always return true for forms inheriting
104 // their parent data, because these forms can never be non-synchronized
105 // (they don't do any data transformation on their own)
106 while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
107 if (!$scope->has($it->current())) {
108 // Break if we find a reference to a non-existing child
112 $scope = $scope->get($it->current());
117 // Follow dot rules until we have the final target
118 $mapping = $scope->getConfig()->getOption('error_mapping');
120 while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
121 $dotRule = new MappingRule($scope, '.', $mapping['.']);
122 $scope = $dotRule->getTarget();
123 $mapping = $scope->getConfig()->getOption('error_mapping');
126 // Only add the error if the form is synchronized
127 if ($this->acceptsErrors($scope)) {
128 $scope->addError(new FormError(
129 $violation->getMessage(),
130 $violation->getMessageTemplate(),
131 $violation->getMessageParameters(),
132 $violation->getMessagePluralization()
138 * Tries to match the beginning of the property path at the
139 * current position against the children of the scope.
141 * If a matching child is found, it is returned. Otherwise
144 * @param FormInterface $form The form to search.
145 * @param PropertyPathIteratorInterface $it The iterator at its current position.
147 * @return null|FormInterface The found match or null.
149 private function matchChild(FormInterface
$form, PropertyPathIteratorInterface
$it)
151 // Remember at what property path underneath "data"
152 // we are looking. Check if there is a child with that
153 // path, otherwise increase path by one more piece
158 // Construct mapping rules for the given form
161 foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
162 // Dot rules are considered at the very end
163 if ('.' !== $propertyPath) {
164 $rules[] = new MappingRule($form, $propertyPath, $targetPath);
168 // Skip forms inheriting their parent data when iterating the children
169 $childIterator = new \
RecursiveIteratorIterator(
170 new InheritDataAwareIterator($form->all())
173 // Make the path longer until we find a matching child
179 if ($it->isIndex()) {
180 $chunk .= '['.$it->current().']';
182 $chunk .= ('' === $chunk ? '' : '.').$it->current();
185 // Test mapping rules as long as we have any
186 foreach ($rules as $key => $rule) {
187 /* @var MappingRule $rule */
189 // Mapping rule matches completely, terminate.
190 if (null !== ($form = $rule->match($chunk))) {
194 // Keep only rules that have $chunk as prefix
195 if (!$rule->isPrefix($chunk)) {
200 // Test children unless we already found one
201 if (null === $foundChild) {
202 foreach ($childIterator as $child) {
203 /* @var FormInterface $child */
204 $childPath = (string) $child->getPropertyPath();
206 // Child found, mark as return value
207 if ($chunk === $childPath) {
208 $foundChild = $child;
209 $foundAtIndex = $it->key();
214 // Add element to the chunk
217 // If we reached the end of the path or if there are no
218 // more matching mapping rules, return the found child
219 if (null !== $foundChild && (!$it->valid() || count($rules) === 0)) {
220 // Reset index in case we tried to find mapping
221 // rules further down the path
222 $it->seek($foundAtIndex);
232 * Reconstructs a property path from a violation path and a form tree.
234 * @param ViolationPath $violationPath The violation path.
235 * @param FormInterface $origin The root form of the tree.
237 * @return RelativePath The reconstructed path.
239 private function reconstructPath(ViolationPath
$violationPath, FormInterface
$origin)
241 $propertyPathBuilder = new PropertyPathBuilder($violationPath);
242 $it = $violationPath->getIterator();
245 // Remember the current index in the builder
248 // Expand elements that map to a form (like "children[address]")
249 for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
250 if (!$scope->has($it->current())) {
251 // Scope relates to a form that does not exist
256 // Process child form
257 $scope = $scope->get($it->current());
259 if ($scope->getConfig()->getInheritData()) {
260 // Form inherits its parent data
261 // Cut the piece out of the property path and proceed
262 $propertyPathBuilder->remove($i);
263 } elseif (!$scope->getConfig()->getMapped()) {
264 // Form is not mapped
265 // Set the form as new origin and strip everything
266 // we have so far in the path
268 $propertyPathBuilder->remove(0, $i +
1);
271 /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
272 $propertyPath = $scope->getPropertyPath();
274 if (null === $propertyPath) {
275 // Property path of a mapped form is null
276 // Should not happen, bail out
280 $propertyPathBuilder->replace($i, 1, $propertyPath);
281 $i +
= $propertyPath->getLength();
285 $finalPath = $propertyPathBuilder->getPropertyPath();
287 return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
291 * @param FormInterface $form
295 private function acceptsErrors(FormInterface
$form)
297 return $this->allowNonSynchronized
|| $form->isSynchronized();