]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
1 | <?php |
2 | ||
3 | /* | |
4 | * This file is part of the Symfony package. | |
5 | * | |
6 | * (c) Fabien Potencier <fabien@symfony.com> | |
7 | * | |
8 | * For the full copyright and license information, please view the LICENSE | |
9 | * file that was distributed with this source code. | |
10 | */ | |
11 | ||
12 | namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; | |
13 | ||
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; | |
22 | ||
23 | /** | |
24 | * @author Bernhard Schussek <bschussek@gmail.com> | |
25 | */ | |
26 | class ViolationMapper implements ViolationMapperInterface | |
27 | { | |
28 | /** | |
29 | * @var Boolean | |
30 | */ | |
31 | private $allowNonSynchronized; | |
32 | ||
33 | /** | |
34 | * {@inheritdoc} | |
35 | */ | |
36 | public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false) | |
37 | { | |
38 | $this->allowNonSynchronized = $allowNonSynchronized; | |
39 | ||
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. | |
45 | $scope = null; | |
46 | ||
47 | $violationPath = null; | |
48 | $relativePath = null; | |
49 | $match = false; | |
50 | ||
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); | |
55 | } | |
56 | ||
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) { | |
60 | $scope = $form; | |
61 | } | |
62 | ||
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. | |
68 | ||
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 | |
73 | // ignored. | |
74 | ||
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); | |
82 | ||
83 | while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) { | |
84 | $scope = $child; | |
85 | $it->next(); | |
86 | $match = true; | |
87 | } | |
88 | } | |
89 | ||
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" | |
99 | ||
100 | $scope = $form; | |
101 | $it = new ViolationPathIterator($violationPath); | |
102 | ||
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 | |
109 | break; | |
110 | } | |
111 | ||
112 | $scope = $scope->get($it->current()); | |
113 | $it->next(); | |
114 | } | |
115 | } | |
116 | ||
117 | // Follow dot rules until we have the final target | |
118 | $mapping = $scope->getConfig()->getOption('error_mapping'); | |
119 | ||
120 | while ($this->acceptsErrors($scope) && isset($mapping['.'])) { | |
121 | $dotRule = new MappingRule($scope, '.', $mapping['.']); | |
122 | $scope = $dotRule->getTarget(); | |
123 | $mapping = $scope->getConfig()->getOption('error_mapping'); | |
124 | } | |
125 | ||
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() | |
133 | )); | |
134 | } | |
135 | } | |
136 | ||
137 | /** | |
138 | * Tries to match the beginning of the property path at the | |
139 | * current position against the children of the scope. | |
140 | * | |
141 | * If a matching child is found, it is returned. Otherwise | |
142 | * null is returned. | |
143 | * | |
144 | * @param FormInterface $form The form to search. | |
145 | * @param PropertyPathIteratorInterface $it The iterator at its current position. | |
146 | * | |
147 | * @return null|FormInterface The found match or null. | |
148 | */ | |
149 | private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it) | |
150 | { | |
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 | |
154 | $chunk = ''; | |
155 | $foundChild = null; | |
156 | $foundAtIndex = 0; | |
157 | ||
158 | // Construct mapping rules for the given form | |
159 | $rules = array(); | |
160 | ||
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); | |
165 | } | |
166 | } | |
167 | ||
168 | // Skip forms inheriting their parent data when iterating the children | |
169 | $childIterator = new \RecursiveIteratorIterator( | |
170 | new InheritDataAwareIterator($form->all()) | |
171 | ); | |
172 | ||
173 | // Make the path longer until we find a matching child | |
174 | while (true) { | |
175 | if (!$it->valid()) { | |
176 | return null; | |
177 | } | |
178 | ||
179 | if ($it->isIndex()) { | |
180 | $chunk .= '['.$it->current().']'; | |
181 | } else { | |
182 | $chunk .= ('' === $chunk ? '' : '.').$it->current(); | |
183 | } | |
184 | ||
185 | // Test mapping rules as long as we have any | |
186 | foreach ($rules as $key => $rule) { | |
187 | /* @var MappingRule $rule */ | |
188 | ||
189 | // Mapping rule matches completely, terminate. | |
190 | if (null !== ($form = $rule->match($chunk))) { | |
191 | return $form; | |
192 | } | |
193 | ||
194 | // Keep only rules that have $chunk as prefix | |
195 | if (!$rule->isPrefix($chunk)) { | |
196 | unset($rules[$key]); | |
197 | } | |
198 | } | |
199 | ||
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(); | |
205 | ||
206 | // Child found, mark as return value | |
207 | if ($chunk === $childPath) { | |
208 | $foundChild = $child; | |
209 | $foundAtIndex = $it->key(); | |
210 | } | |
211 | } | |
212 | } | |
213 | ||
214 | // Add element to the chunk | |
215 | $it->next(); | |
216 | ||
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); | |
223 | ||
224 | return $foundChild; | |
225 | } | |
226 | } | |
227 | ||
228 | return null; | |
229 | } | |
230 | ||
231 | /** | |
232 | * Reconstructs a property path from a violation path and a form tree. | |
233 | * | |
234 | * @param ViolationPath $violationPath The violation path. | |
235 | * @param FormInterface $origin The root form of the tree. | |
236 | * | |
237 | * @return RelativePath The reconstructed path. | |
238 | */ | |
239 | private function reconstructPath(ViolationPath $violationPath, FormInterface $origin) | |
240 | { | |
241 | $propertyPathBuilder = new PropertyPathBuilder($violationPath); | |
242 | $it = $violationPath->getIterator(); | |
243 | $scope = $origin; | |
244 | ||
245 | // Remember the current index in the builder | |
246 | $i = 0; | |
247 | ||
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 | |
252 | // Bail out | |
253 | break; | |
254 | } | |
255 | ||
256 | // Process child form | |
257 | $scope = $scope->get($it->current()); | |
258 | ||
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 | |
267 | $origin = $scope; | |
268 | $propertyPathBuilder->remove(0, $i + 1); | |
269 | $i = 0; | |
270 | } else { | |
271 | /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */ | |
272 | $propertyPath = $scope->getPropertyPath(); | |
273 | ||
274 | if (null === $propertyPath) { | |
275 | // Property path of a mapped form is null | |
276 | // Should not happen, bail out | |
277 | break; | |
278 | } | |
279 | ||
280 | $propertyPathBuilder->replace($i, 1, $propertyPath); | |
281 | $i += $propertyPath->getLength(); | |
282 | } | |
283 | } | |
284 | ||
285 | $finalPath = $propertyPathBuilder->getPropertyPath(); | |
286 | ||
287 | return null !== $finalPath ? new RelativePath($origin, $finalPath) : null; | |
288 | } | |
289 | ||
290 | /** | |
291 | * @param FormInterface $form | |
292 | * | |
293 | * @return Boolean | |
294 | */ | |
295 | private function acceptsErrors(FormInterface $form) | |
296 | { | |
297 | return $this->allowNonSynchronized || $form->isSynchronized(); | |
298 | } | |
299 | } |