ViolationMapper.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Form\Extension\Validator\ViolationMapper;
  11. use Symfony\Component\Form\FormError;
  12. use Symfony\Component\Form\FormInterface;
  13. use Symfony\Component\Form\Util\InheritDataAwareIterator;
  14. use Symfony\Component\PropertyAccess\PropertyPathBuilder;
  15. use Symfony\Component\PropertyAccess\PropertyPathIterator;
  16. use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
  17. use Symfony\Component\Validator\ConstraintViolation;
  18. /**
  19. * @author Bernhard Schussek <bschussek@gmail.com>
  20. */
  21. class ViolationMapper implements ViolationMapperInterface
  22. {
  23. /**
  24. * @var bool
  25. */
  26. private $allowNonSynchronized;
  27. /**
  28. * {@inheritdoc}
  29. */
  30. public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false)
  31. {
  32. $this->allowNonSynchronized = $allowNonSynchronized;
  33. // The scope is the currently found most specific form that
  34. // an error should be mapped to. After setting the scope, the
  35. // mapper will try to continue to find more specific matches in
  36. // the children of scope. If it cannot, the error will be
  37. // mapped to this scope.
  38. $scope = null;
  39. $violationPath = null;
  40. $relativePath = null;
  41. $match = false;
  42. // Don't create a ViolationPath instance for empty property paths
  43. if (\strlen($violation->getPropertyPath()) > 0) {
  44. $violationPath = new ViolationPath($violation->getPropertyPath());
  45. $relativePath = $this->reconstructPath($violationPath, $form);
  46. }
  47. // This case happens if the violation path is empty and thus
  48. // the violation should be mapped to the root form
  49. if (null === $violationPath) {
  50. $scope = $form;
  51. }
  52. // In general, mapping happens from the root form to the leaf forms
  53. // First, the rules of the root form are applied to determine
  54. // the subsequent descendant. The rules of this descendant are then
  55. // applied to find the next and so on, until we have found the
  56. // most specific form that matches the violation.
  57. // If any of the forms found in this process is not synchronized,
  58. // mapping is aborted. Non-synchronized forms could not reverse
  59. // transform the value entered by the user, thus any further violations
  60. // caused by the (invalid) reverse transformed value should be
  61. // ignored.
  62. if (null !== $relativePath) {
  63. // Set the scope to the root of the relative path
  64. // This root will usually be $form. If the path contains
  65. // an unmapped form though, the last unmapped form found
  66. // will be the root of the path.
  67. $scope = $relativePath->getRoot();
  68. $it = new PropertyPathIterator($relativePath);
  69. while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
  70. $scope = $child;
  71. $it->next();
  72. $match = true;
  73. }
  74. }
  75. // This case happens if an error happened in the data under a
  76. // form inheriting its parent data that does not match any of the
  77. // children of that form.
  78. if (null !== $violationPath && !$match) {
  79. // If we could not map the error to anything more specific
  80. // than the root element, map it to the innermost directly
  81. // mapped form of the violation path
  82. // e.g. "children[foo].children[bar].data.baz"
  83. // Here the innermost directly mapped child is "bar"
  84. $scope = $form;
  85. $it = new ViolationPathIterator($violationPath);
  86. // Note: acceptsErrors() will always return true for forms inheriting
  87. // their parent data, because these forms can never be non-synchronized
  88. // (they don't do any data transformation on their own)
  89. while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
  90. if (!$scope->has($it->current())) {
  91. // Break if we find a reference to a non-existing child
  92. break;
  93. }
  94. $scope = $scope->get($it->current());
  95. $it->next();
  96. }
  97. }
  98. // Follow dot rules until we have the final target
  99. $mapping = $scope->getConfig()->getOption('error_mapping');
  100. while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
  101. $dotRule = new MappingRule($scope, '.', $mapping['.']);
  102. $scope = $dotRule->getTarget();
  103. $mapping = $scope->getConfig()->getOption('error_mapping');
  104. }
  105. // Only add the error if the form is synchronized
  106. if ($this->acceptsErrors($scope)) {
  107. $scope->addError(new FormError(
  108. $violation->getMessage(),
  109. $violation->getMessageTemplate(),
  110. $violation->getParameters(),
  111. $violation->getPlural(),
  112. $violation
  113. ));
  114. }
  115. }
  116. /**
  117. * Tries to match the beginning of the property path at the
  118. * current position against the children of the scope.
  119. *
  120. * If a matching child is found, it is returned. Otherwise
  121. * null is returned.
  122. *
  123. * @param FormInterface $form The form to search
  124. * @param PropertyPathIteratorInterface $it The iterator at its current position
  125. *
  126. * @return FormInterface|null The found match or null
  127. */
  128. private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it)
  129. {
  130. $target = null;
  131. $chunk = '';
  132. $foundAtIndex = null;
  133. // Construct mapping rules for the given form
  134. $rules = array();
  135. foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
  136. // Dot rules are considered at the very end
  137. if ('.' !== $propertyPath) {
  138. $rules[] = new MappingRule($form, $propertyPath, $targetPath);
  139. }
  140. }
  141. $children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false);
  142. while ($it->valid()) {
  143. if ($it->isIndex()) {
  144. $chunk .= '['.$it->current().']';
  145. } else {
  146. $chunk .= ('' === $chunk ? '' : '.').$it->current();
  147. }
  148. // Test mapping rules as long as we have any
  149. foreach ($rules as $key => $rule) {
  150. /* @var MappingRule $rule */
  151. // Mapping rule matches completely, terminate.
  152. if (null !== ($form = $rule->match($chunk))) {
  153. return $form;
  154. }
  155. // Keep only rules that have $chunk as prefix
  156. if (!$rule->isPrefix($chunk)) {
  157. unset($rules[$key]);
  158. }
  159. }
  160. /** @var FormInterface $child */
  161. foreach ($children as $i => $child) {
  162. $childPath = (string) $child->getPropertyPath();
  163. if ($childPath === $chunk) {
  164. $target = $child;
  165. $foundAtIndex = $it->key();
  166. } elseif (0 === strpos($childPath, $chunk)) {
  167. continue;
  168. }
  169. unset($children[$i]);
  170. }
  171. $it->next();
  172. }
  173. if (null !== $foundAtIndex) {
  174. $it->seek($foundAtIndex);
  175. }
  176. return $target;
  177. }
  178. /**
  179. * Reconstructs a property path from a violation path and a form tree.
  180. *
  181. * @param ViolationPath $violationPath The violation path
  182. * @param FormInterface $origin The root form of the tree
  183. *
  184. * @return RelativePath The reconstructed path
  185. */
  186. private function reconstructPath(ViolationPath $violationPath, FormInterface $origin)
  187. {
  188. $propertyPathBuilder = new PropertyPathBuilder($violationPath);
  189. $it = $violationPath->getIterator();
  190. $scope = $origin;
  191. // Remember the current index in the builder
  192. $i = 0;
  193. // Expand elements that map to a form (like "children[address]")
  194. for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
  195. if (!$scope->has($it->current())) {
  196. // Scope relates to a form that does not exist
  197. // Bail out
  198. break;
  199. }
  200. // Process child form
  201. $scope = $scope->get($it->current());
  202. if ($scope->getConfig()->getInheritData()) {
  203. // Form inherits its parent data
  204. // Cut the piece out of the property path and proceed
  205. $propertyPathBuilder->remove($i);
  206. } elseif (!$scope->getConfig()->getMapped()) {
  207. // Form is not mapped
  208. // Set the form as new origin and strip everything
  209. // we have so far in the path
  210. $origin = $scope;
  211. $propertyPathBuilder->remove(0, $i + 1);
  212. $i = 0;
  213. } else {
  214. /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
  215. $propertyPath = $scope->getPropertyPath();
  216. if (null === $propertyPath) {
  217. // Property path of a mapped form is null
  218. // Should not happen, bail out
  219. break;
  220. }
  221. $propertyPathBuilder->replace($i, 1, $propertyPath);
  222. $i += $propertyPath->getLength();
  223. }
  224. }
  225. $finalPath = $propertyPathBuilder->getPropertyPath();
  226. return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
  227. }
  228. /**
  229. * @return bool
  230. */
  231. private function acceptsErrors(FormInterface $form)
  232. {
  233. // Ignore non-submitted forms. This happens, for example, in PATCH
  234. // requests.
  235. // https://github.com/symfony/symfony/pull/10567
  236. return $form->isSubmitted() && ($this->allowNonSynchronized || $form->isSynchronized());
  237. }
  238. }