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