vendor/symfony/form/FormErrorIterator.php line 38

  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;
  11. use Symfony\Component\Form\Exception\BadMethodCallException;
  12. use Symfony\Component\Form\Exception\InvalidArgumentException;
  13. use Symfony\Component\Form\Exception\LogicException;
  14. use Symfony\Component\Form\Exception\OutOfBoundsException;
  15. use Symfony\Component\Validator\ConstraintViolation;
  16. /**
  17. * Iterates over the errors of a form.
  18. *
  19. * This class supports recursive iteration. In order to iterate recursively,
  20. * pass a structure of {@link FormError} and {@link FormErrorIterator} objects
  21. * to the $errors constructor argument.
  22. *
  23. * You can also wrap the iterator into a {@link \RecursiveIteratorIterator} to
  24. * flatten the recursive structure into a flat list of errors.
  25. *
  26. * @author Bernhard Schussek <bschussek@gmail.com>
  27. *
  28. * @template T of FormError|FormErrorIterator
  29. *
  30. * @implements \ArrayAccess<int, T>
  31. * @implements \RecursiveIterator<int, T>
  32. * @implements \SeekableIterator<int, T>
  33. */
  34. class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable, \Stringable
  35. {
  36. /**
  37. * The prefix used for indenting nested error messages.
  38. */
  39. public const INDENTATION = ' ';
  40. private FormInterface $form;
  41. /**
  42. * @var list<T>
  43. */
  44. private array $errors;
  45. /**
  46. * @param list<T> $errors
  47. *
  48. * @throws InvalidArgumentException If the errors are invalid
  49. */
  50. public function __construct(FormInterface $form, array $errors)
  51. {
  52. foreach ($errors as $error) {
  53. if (!($error instanceof FormError || $error instanceof self)) {
  54. throw new InvalidArgumentException(\sprintf('The errors must be instances of "Symfony\Component\Form\FormError" or "%s". Got: "%s".', __CLASS__, get_debug_type($error)));
  55. }
  56. }
  57. $this->form = $form;
  58. $this->errors = $errors;
  59. }
  60. /**
  61. * Returns all iterated error messages as string.
  62. */
  63. public function __toString(): string
  64. {
  65. $string = '';
  66. foreach ($this->errors as $error) {
  67. if ($error instanceof FormError) {
  68. $string .= 'ERROR: '.$error->getMessage()."\n";
  69. } else {
  70. /** @var self $error */
  71. $string .= $error->getForm()->getName().":\n";
  72. $string .= self::indent((string) $error);
  73. }
  74. }
  75. return $string;
  76. }
  77. /**
  78. * Returns the iterated form.
  79. */
  80. public function getForm(): FormInterface
  81. {
  82. return $this->form;
  83. }
  84. /**
  85. * Returns the current element of the iterator.
  86. *
  87. * @return T An error or an iterator containing nested errors
  88. */
  89. public function current(): FormError|self
  90. {
  91. return current($this->errors);
  92. }
  93. /**
  94. * Advances the iterator to the next position.
  95. */
  96. public function next(): void
  97. {
  98. next($this->errors);
  99. }
  100. /**
  101. * Returns the current position of the iterator.
  102. */
  103. public function key(): int
  104. {
  105. return key($this->errors);
  106. }
  107. /**
  108. * Returns whether the iterator's position is valid.
  109. */
  110. public function valid(): bool
  111. {
  112. return null !== key($this->errors);
  113. }
  114. /**
  115. * Sets the iterator's position to the beginning.
  116. *
  117. * This method detects if errors have been added to the form since the
  118. * construction of the iterator.
  119. */
  120. public function rewind(): void
  121. {
  122. reset($this->errors);
  123. }
  124. /**
  125. * Returns whether a position exists in the iterator.
  126. *
  127. * @param int $position The position
  128. */
  129. public function offsetExists(mixed $position): bool
  130. {
  131. return isset($this->errors[$position]);
  132. }
  133. /**
  134. * Returns the element at a position in the iterator.
  135. *
  136. * @param int $position The position
  137. *
  138. * @return T
  139. *
  140. * @throws OutOfBoundsException If the given position does not exist
  141. */
  142. public function offsetGet(mixed $position): FormError|self
  143. {
  144. if (!isset($this->errors[$position])) {
  145. throw new OutOfBoundsException('The offset '.$position.' does not exist.');
  146. }
  147. return $this->errors[$position];
  148. }
  149. /**
  150. * Unsupported method.
  151. *
  152. * @throws BadMethodCallException
  153. */
  154. public function offsetSet(mixed $position, mixed $value): void
  155. {
  156. throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
  157. }
  158. /**
  159. * Unsupported method.
  160. *
  161. * @throws BadMethodCallException
  162. */
  163. public function offsetUnset(mixed $position): void
  164. {
  165. throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
  166. }
  167. /**
  168. * Returns whether the current element of the iterator can be recursed
  169. * into.
  170. */
  171. public function hasChildren(): bool
  172. {
  173. return current($this->errors) instanceof self;
  174. }
  175. public function getChildren(): self
  176. {
  177. if (!$this->hasChildren()) {
  178. throw new LogicException(\sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()'));
  179. }
  180. /** @var self $children */
  181. $children = current($this->errors);
  182. return $children;
  183. }
  184. /**
  185. * Returns the number of elements in the iterator.
  186. *
  187. * Note that this is not the total number of errors, if the constructor
  188. * parameter $deep was set to true! In that case, you should wrap the
  189. * iterator into a {@link \RecursiveIteratorIterator} with the standard mode
  190. * {@link \RecursiveIteratorIterator::LEAVES_ONLY} and count the result.
  191. *
  192. * $iterator = new \RecursiveIteratorIterator($form->getErrors(true));
  193. * $count = count(iterator_to_array($iterator));
  194. *
  195. * Alternatively, set the constructor argument $flatten to true as well.
  196. *
  197. * $count = count($form->getErrors(true, true));
  198. */
  199. public function count(): int
  200. {
  201. return \count($this->errors);
  202. }
  203. /**
  204. * Sets the position of the iterator.
  205. *
  206. * @throws OutOfBoundsException If the position is invalid
  207. */
  208. public function seek(int $position): void
  209. {
  210. if (!isset($this->errors[$position])) {
  211. throw new OutOfBoundsException('The offset '.$position.' does not exist.');
  212. }
  213. reset($this->errors);
  214. while ($position !== key($this->errors)) {
  215. next($this->errors);
  216. }
  217. }
  218. /**
  219. * Creates iterator for errors with specific codes.
  220. *
  221. * @param string|string[] $codes The codes to find
  222. */
  223. public function findByCodes(string|array $codes): static
  224. {
  225. $codes = (array) $codes;
  226. $errors = [];
  227. foreach ($this as $error) {
  228. $cause = $error->getCause();
  229. if ($cause instanceof ConstraintViolation && \in_array($cause->getCode(), $codes, true)) {
  230. $errors[] = $error;
  231. }
  232. }
  233. return new static($this->form, $errors);
  234. }
  235. /**
  236. * Utility function for indenting multi-line strings.
  237. */
  238. private static function indent(string $string): string
  239. {
  240. return rtrim(self::INDENTATION.str_replace("\n", "\n".self::INDENTATION, $string), ' ');
  241. }
  242. }