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