FormErrorIterator.php 7.5 KB

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