ViolationMapper.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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\FileUploadError;
  12. use Symfony\Component\Form\FormError;
  13. use Symfony\Component\Form\FormInterface;
  14. use Symfony\Component\Form\FormRendererInterface;
  15. use Symfony\Component\Form\Util\InheritDataAwareIterator;
  16. use Symfony\Component\PropertyAccess\PropertyPathBuilder;
  17. use Symfony\Component\PropertyAccess\PropertyPathIterator;
  18. use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
  19. use Symfony\Component\Validator\Constraints\File;
  20. use Symfony\Component\Validator\ConstraintViolation;
  21. use Symfony\Contracts\Translation\TranslatorInterface;
  22. /**
  23. * @author Bernhard Schussek <bschussek@gmail.com>
  24. */
  25. class ViolationMapper implements ViolationMapperInterface
  26. {
  27. private $formRenderer;
  28. private $translator;
  29. private $allowNonSynchronized = false;
  30. public function __construct(FormRendererInterface $formRenderer = null, TranslatorInterface $translator = null)
  31. {
  32. $this->formRenderer = $formRenderer;
  33. $this->translator = $translator;
  34. }
  35. /**
  36. * {@inheritdoc}
  37. */
  38. public function mapViolation(ConstraintViolation $violation, FormInterface $form, bool $allowNonSynchronized = false)
  39. {
  40. $this->allowNonSynchronized = $allowNonSynchronized;
  41. // The scope is the currently found most specific form that
  42. // an error should be mapped to. After setting the scope, the
  43. // mapper will try to continue to find more specific matches in
  44. // the children of scope. If it cannot, the error will be
  45. // mapped to this scope.
  46. $scope = null;
  47. $violationPath = null;
  48. $relativePath = null;
  49. $match = false;
  50. // Don't create a ViolationPath instance for empty property paths
  51. if (\strlen($violation->getPropertyPath()) > 0) {
  52. $violationPath = new ViolationPath($violation->getPropertyPath());
  53. $relativePath = $this->reconstructPath($violationPath, $form);
  54. }
  55. // This case happens if the violation path is empty and thus
  56. // the violation should be mapped to the root form
  57. if (null === $violationPath) {
  58. $scope = $form;
  59. }
  60. // In general, mapping happens from the root form to the leaf forms
  61. // First, the rules of the root form are applied to determine
  62. // the subsequent descendant. The rules of this descendant are then
  63. // applied to find the next and so on, until we have found the
  64. // most specific form that matches the violation.
  65. // If any of the forms found in this process is not synchronized,
  66. // mapping is aborted. Non-synchronized forms could not reverse
  67. // transform the value entered by the user, thus any further violations
  68. // caused by the (invalid) reverse transformed value should be
  69. // ignored.
  70. if (null !== $relativePath) {
  71. // Set the scope to the root of the relative path
  72. // This root will usually be $form. If the path contains
  73. // an unmapped form though, the last unmapped form found
  74. // will be the root of the path.
  75. $scope = $relativePath->getRoot();
  76. $it = new PropertyPathIterator($relativePath);
  77. while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
  78. $scope = $child;
  79. $it->next();
  80. $match = true;
  81. }
  82. }
  83. // This case happens if an error happened in the data under a
  84. // form inheriting its parent data that does not match any of the
  85. // children of that form.
  86. if (null !== $violationPath && !$match) {
  87. // If we could not map the error to anything more specific
  88. // than the root element, map it to the innermost directly
  89. // mapped form of the violation path
  90. // e.g. "children[foo].children[bar].data.baz"
  91. // Here the innermost directly mapped child is "bar"
  92. $scope = $form;
  93. $it = new ViolationPathIterator($violationPath);
  94. // Note: acceptsErrors() will always return true for forms inheriting
  95. // their parent data, because these forms can never be non-synchronized
  96. // (they don't do any data transformation on their own)
  97. while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
  98. if (!$scope->has($it->current())) {
  99. // Break if we find a reference to a non-existing child
  100. break;
  101. }
  102. $scope = $scope->get($it->current());
  103. $it->next();
  104. }
  105. }
  106. // Follow dot rules until we have the final target
  107. $mapping = $scope->getConfig()->getOption('error_mapping');
  108. while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
  109. $dotRule = new MappingRule($scope, '.', $mapping['.']);
  110. $scope = $dotRule->getTarget();
  111. $mapping = $scope->getConfig()->getOption('error_mapping');
  112. }
  113. // Only add the error if the form is synchronized
  114. if ($this->acceptsErrors($scope)) {
  115. if ($violation->getConstraint() instanceof File && (string) \UPLOAD_ERR_INI_SIZE === $violation->getCode()) {
  116. $errorsTarget = $scope;
  117. while (null !== $errorsTarget->getParent() && $errorsTarget->getConfig()->getErrorBubbling()) {
  118. $errorsTarget = $errorsTarget->getParent();
  119. }
  120. $errors = $errorsTarget->getErrors();
  121. $errorsTarget->clearErrors();
  122. foreach ($errors as $error) {
  123. if (!$error instanceof FileUploadError) {
  124. $errorsTarget->addError($error);
  125. }
  126. }
  127. }
  128. $message = $violation->getMessage();
  129. $messageTemplate = $violation->getMessageTemplate();
  130. if (false !== strpos($message, '{{ label }}') || false !== strpos($messageTemplate, '{{ label }}')) {
  131. $form = $scope;
  132. do {
  133. $labelFormat = $form->getConfig()->getOption('label_format');
  134. } while (null === $labelFormat && null !== $form = $form->getParent());
  135. if (null !== $labelFormat) {
  136. $label = str_replace(
  137. [
  138. '%name%',
  139. '%id%',
  140. ],
  141. [
  142. $scope->getName(),
  143. (string) $scope->getPropertyPath(),
  144. ],
  145. $labelFormat
  146. );
  147. } else {
  148. $label = $scope->getConfig()->getOption('label');
  149. }
  150. if (false !== $label) {
  151. if (null === $label && null !== $this->formRenderer) {
  152. $label = $this->formRenderer->humanize($scope->getName());
  153. } elseif (null === $label) {
  154. $label = $scope->getName();
  155. }
  156. if (null !== $this->translator) {
  157. $form = $scope;
  158. $translationParameters = $form->getConfig()->getOption('label_translation_parameters', []);
  159. do {
  160. $translationDomain = $form->getConfig()->getOption('translation_domain');
  161. $translationParameters = array_merge($form->getConfig()->getOption('label_translation_parameters', []), $translationParameters);
  162. } while (null === $translationDomain && null !== $form = $form->getParent());
  163. $label = $this->translator->trans(
  164. $label,
  165. $translationParameters,
  166. $translationDomain
  167. );
  168. }
  169. $message = str_replace('{{ label }}', $label, $message);
  170. $messageTemplate = str_replace('{{ label }}', $label, $messageTemplate);
  171. }
  172. }
  173. $scope->addError(new FormError(
  174. $message,
  175. $messageTemplate,
  176. $violation->getParameters(),
  177. $violation->getPlural(),
  178. $violation
  179. ));
  180. }
  181. }
  182. /**
  183. * Tries to match the beginning of the property path at the
  184. * current position against the children of the scope.
  185. *
  186. * If a matching child is found, it is returned. Otherwise
  187. * null is returned.
  188. */
  189. private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it): ?FormInterface
  190. {
  191. $target = null;
  192. $chunk = '';
  193. $foundAtIndex = null;
  194. // Construct mapping rules for the given form
  195. $rules = [];
  196. foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
  197. // Dot rules are considered at the very end
  198. if ('.' !== $propertyPath) {
  199. $rules[] = new MappingRule($form, $propertyPath, $targetPath);
  200. }
  201. }
  202. $children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false);
  203. while ($it->valid()) {
  204. if ($it->isIndex()) {
  205. $chunk .= '['.$it->current().']';
  206. } else {
  207. $chunk .= ('' === $chunk ? '' : '.').$it->current();
  208. }
  209. // Test mapping rules as long as we have any
  210. foreach ($rules as $key => $rule) {
  211. /* @var MappingRule $rule */
  212. // Mapping rule matches completely, terminate.
  213. if (null !== ($form = $rule->match($chunk))) {
  214. return $form;
  215. }
  216. // Keep only rules that have $chunk as prefix
  217. if (!$rule->isPrefix($chunk)) {
  218. unset($rules[$key]);
  219. }
  220. }
  221. /** @var FormInterface $child */
  222. foreach ($children as $i => $child) {
  223. $childPath = (string) $child->getPropertyPath();
  224. if ($childPath === $chunk) {
  225. $target = $child;
  226. $foundAtIndex = $it->key();
  227. } elseif (0 === strpos($childPath, $chunk)) {
  228. continue;
  229. }
  230. unset($children[$i]);
  231. }
  232. $it->next();
  233. }
  234. if (null !== $foundAtIndex) {
  235. $it->seek($foundAtIndex);
  236. }
  237. return $target;
  238. }
  239. /**
  240. * Reconstructs a property path from a violation path and a form tree.
  241. */
  242. private function reconstructPath(ViolationPath $violationPath, FormInterface $origin): ?RelativePath
  243. {
  244. $propertyPathBuilder = new PropertyPathBuilder($violationPath);
  245. $it = $violationPath->getIterator();
  246. $scope = $origin;
  247. // Remember the current index in the builder
  248. $i = 0;
  249. // Expand elements that map to a form (like "children[address]")
  250. for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
  251. if (!$scope->has($it->current())) {
  252. // Scope relates to a form that does not exist
  253. // Bail out
  254. break;
  255. }
  256. // Process child form
  257. $scope = $scope->get($it->current());
  258. if ($scope->getConfig()->getInheritData()) {
  259. // Form inherits its parent data
  260. // Cut the piece out of the property path and proceed
  261. $propertyPathBuilder->remove($i);
  262. } else {
  263. /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
  264. $propertyPath = $scope->getPropertyPath();
  265. if (null === $propertyPath) {
  266. // Property path of a mapped form is null
  267. // Should not happen, bail out
  268. break;
  269. }
  270. $propertyPathBuilder->replace($i, 1, $propertyPath);
  271. $i += $propertyPath->getLength();
  272. }
  273. }
  274. $finalPath = $propertyPathBuilder->getPropertyPath();
  275. return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
  276. }
  277. private function acceptsErrors(FormInterface $form): bool
  278. {
  279. return $this->allowNonSynchronized || $form->isSynchronized();
  280. }
  281. }