ViolationPath.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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\Exception\OutOfBoundsException;
  12. use Symfony\Component\PropertyAccess\PropertyPath;
  13. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  14. /**
  15. * @author Bernhard Schussek <bschussek@gmail.com>
  16. */
  17. class ViolationPath implements \IteratorAggregate, PropertyPathInterface
  18. {
  19. /**
  20. * @var array
  21. */
  22. private $elements = [];
  23. /**
  24. * @var array
  25. */
  26. private $isIndex = [];
  27. /**
  28. * @var array
  29. */
  30. private $mapsForm = [];
  31. /**
  32. * @var string
  33. */
  34. private $pathAsString = '';
  35. /**
  36. * @var int
  37. */
  38. private $length = 0;
  39. /**
  40. * Creates a new violation path from a string.
  41. *
  42. * @param string $violationPath The property path of a {@link \Symfony\Component\Validator\ConstraintViolation} object
  43. */
  44. public function __construct(string $violationPath)
  45. {
  46. $path = new PropertyPath($violationPath);
  47. $elements = $path->getElements();
  48. $data = false;
  49. for ($i = 0, $l = \count($elements); $i < $l; ++$i) {
  50. if (!$data) {
  51. // The element "data" has not yet been passed
  52. if ('children' === $elements[$i] && $path->isProperty($i)) {
  53. // Skip element "children"
  54. ++$i;
  55. // Next element must exist and must be an index
  56. // Otherwise consider this the end of the path
  57. if ($i >= $l || !$path->isIndex($i)) {
  58. break;
  59. }
  60. // All the following index items (regardless if .children is
  61. // explicitly used) are children and grand-children
  62. for (; $i < $l && $path->isIndex($i); ++$i) {
  63. $this->elements[] = $elements[$i];
  64. $this->isIndex[] = true;
  65. $this->mapsForm[] = true;
  66. }
  67. // Rewind the pointer as the last element above didn't match
  68. // (even if the pointer was moved forward)
  69. --$i;
  70. } elseif ('data' === $elements[$i] && $path->isProperty($i)) {
  71. // Skip element "data"
  72. ++$i;
  73. // End of path
  74. if ($i >= $l) {
  75. break;
  76. }
  77. $this->elements[] = $elements[$i];
  78. $this->isIndex[] = $path->isIndex($i);
  79. $this->mapsForm[] = false;
  80. $data = true;
  81. } else {
  82. // Neither "children" nor "data" property found
  83. // Consider this the end of the path
  84. break;
  85. }
  86. } else {
  87. // Already after the "data" element
  88. // Pick everything as is
  89. $this->elements[] = $elements[$i];
  90. $this->isIndex[] = $path->isIndex($i);
  91. $this->mapsForm[] = false;
  92. }
  93. }
  94. $this->length = \count($this->elements);
  95. $this->buildString();
  96. }
  97. /**
  98. * {@inheritdoc}
  99. */
  100. public function __toString()
  101. {
  102. return $this->pathAsString;
  103. }
  104. /**
  105. * {@inheritdoc}
  106. */
  107. public function getLength()
  108. {
  109. return $this->length;
  110. }
  111. /**
  112. * {@inheritdoc}
  113. */
  114. public function getParent()
  115. {
  116. if ($this->length <= 1) {
  117. return null;
  118. }
  119. $parent = clone $this;
  120. --$parent->length;
  121. array_pop($parent->elements);
  122. array_pop($parent->isIndex);
  123. array_pop($parent->mapsForm);
  124. $parent->buildString();
  125. return $parent;
  126. }
  127. /**
  128. * {@inheritdoc}
  129. */
  130. public function getElements()
  131. {
  132. return $this->elements;
  133. }
  134. /**
  135. * {@inheritdoc}
  136. */
  137. public function getElement(int $index)
  138. {
  139. if (!isset($this->elements[$index])) {
  140. throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index));
  141. }
  142. return $this->elements[$index];
  143. }
  144. /**
  145. * {@inheritdoc}
  146. */
  147. public function isProperty(int $index)
  148. {
  149. if (!isset($this->isIndex[$index])) {
  150. throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index));
  151. }
  152. return !$this->isIndex[$index];
  153. }
  154. /**
  155. * {@inheritdoc}
  156. */
  157. public function isIndex(int $index)
  158. {
  159. if (!isset($this->isIndex[$index])) {
  160. throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index));
  161. }
  162. return $this->isIndex[$index];
  163. }
  164. /**
  165. * Returns whether an element maps directly to a form.
  166. *
  167. * Consider the following violation path:
  168. *
  169. * children[address].children[office].data.street
  170. *
  171. * In this example, "address" and "office" map to forms, while
  172. * "street does not.
  173. *
  174. * @return bool Whether the element maps to a form
  175. *
  176. * @throws OutOfBoundsException if the offset is invalid
  177. */
  178. public function mapsForm(int $index)
  179. {
  180. if (!isset($this->mapsForm[$index])) {
  181. throw new OutOfBoundsException(sprintf('The index "%s" is not within the violation path.', $index));
  182. }
  183. return $this->mapsForm[$index];
  184. }
  185. /**
  186. * Returns a new iterator for this path.
  187. *
  188. * @return ViolationPathIterator
  189. */
  190. public function getIterator()
  191. {
  192. return new ViolationPathIterator($this);
  193. }
  194. /**
  195. * Builds the string representation from the elements.
  196. */
  197. private function buildString()
  198. {
  199. $this->pathAsString = '';
  200. $data = false;
  201. foreach ($this->elements as $index => $element) {
  202. if ($this->mapsForm[$index]) {
  203. $this->pathAsString .= ".children[$element]";
  204. } elseif (!$data) {
  205. $this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element");
  206. $data = true;
  207. } else {
  208. $this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element";
  209. }
  210. }
  211. if ('' !== $this->pathAsString) {
  212. // remove leading dot
  213. $this->pathAsString = substr($this->pathAsString, 1);
  214. }
  215. }
  216. }