[Form] Changed Form::getErrors() to return an iterator and added two optional parameters $deep and $flatten

This commit is contained in:
Bernhard Schussek 2013-12-31 17:24:28 +01:00
parent 8ea3a43167
commit a9268c4a99
11 changed files with 516 additions and 40 deletions

View File

@ -5,3 +5,42 @@ Routing
-------
* Added a new optional parameter `$requiredSchemes` to `Symfony\Component\Routing\Generator\UrlGenerator::doGenerate()`
Form
----
* The method `FormInterface::getErrors()` now returns an instance of
`Symfony\Component\Form\FormErrorIterator` instead of an array. This object
is traversable, countable and supports array access. However, you can not
pass it to any of PHP's `array_*` functions anymore. You should use
`iterator_to_array()` in those cases where you did.
Before:
```
$errors = array_map($callback, $form->getErrors());
```
After:
```
$errors = array_map($callback, iterator_to_array($form->getErrors()));
```
* The method `FormInterface::getErrors()` now has two additional, optional
parameters. Make sure to add these parameters to the method signatures of
your implementations of that interface.
Before:
```
public function getErrors()
{
```
After:
```
public function getErrors($deep = false, $flatten = false)
{
```

View File

@ -180,6 +180,22 @@ UPGRADE FROM 2.x to 3.0
* The options "csrf_provider" and "intention" were renamed to "csrf_token_generator"
and "csrf_token_id".
* The method `Form::getErrorsAsString()` was removed. Use `Form::getErrors()`
instead with the argument `$deep` set to true and cast the returned iterator
to a string (if not done implicitly by PHP).
Before:
```
echo $form->getErrorsAsString();
```
After:
```
echo $form->getErrors(true);
```
### FrameworkBundle

View File

@ -1,4 +1,4 @@
<?php if ($errors): ?>
<?php if (count($errors) > 0): ?>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo $error->getMessage() ?></li>

View File

@ -184,9 +184,11 @@ class Button implements \IteratorAggregate, FormInterface
/**
* {@inheritdoc}
*/
public function getErrors()
public function getErrors($deep = false, $flatten = false)
{
return array();
$errors = array();
return new FormErrorIterator($errors, $this, $deep, $flatten);
}
/**

View File

@ -7,6 +7,9 @@ CHANGELOG
* added an option for multiple files upload
* form errors now reference their cause (constraint violation, exception, ...)
* form errors now remember which form they were originally added to
* [BC BREAK] added two optional parameters to FormInterface::getErrors() and
changed the method to return a Symfony\Component\Form\FormErrorIterator
instance instead of an array
2.4.0
-----

View File

@ -778,9 +778,9 @@ class Form implements \IteratorAggregate, FormInterface
/**
* {@inheritdoc}
*/
public function getErrors()
public function getErrors($deep = false, $flatten = false)
{
return $this->errors;
return new FormErrorIterator($this->errors, $this, $deep, $flatten);
}
/**
@ -791,24 +791,13 @@ class Form implements \IteratorAggregate, FormInterface
* @param integer $level The indentation level (used internally)
*
* @return string A string representation of all errors
*
* @deprecated Deprecated since version 2.5, to be removed in 3.0. Use
* {@link getErrors()} instead and cast the result to a string.
*/
public function getErrorsAsString($level = 0)
{
$errors = '';
foreach ($this->errors as $error) {
$errors .= str_repeat(' ', $level).'ERROR: '.$error->getMessage()."\n";
}
foreach ($this->children as $key => $child) {
$errors .= str_repeat(' ', $level).$key.":\n";
if ($child instanceof self && $err = $child->getErrorsAsString($level + 4)) {
$errors .= $err;
} else {
$errors .= str_repeat(' ', $level + 4)."No errors\n";
}
}
return $errors;
return self::indent((string) $this->getErrors(true), $level);
}
/**
@ -1115,4 +1104,19 @@ class Form implements \IteratorAggregate, FormInterface
return $value;
}
/**
* Utility function for indenting multi-line strings.
*
* @param string $string The string
* @param integer $level The number of spaces to use for indentation
*
* @return string The indented string
*/
private static function indent($string, $level)
{
$indentation = str_repeat(' ', $level);
return rtrim($indentation.str_replace("\n", "\n".$indentation, $string), ' ');
}
}

View File

@ -0,0 +1,310 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\OutOfBoundsException;
use Symfony\Component\Form\Exception\BadMethodCallException;
/**
* Iterates over the errors of a form.
*
* Optionally, this class supports recursive iteration. In order to iterate
* recursively, set the constructor argument $deep to true. Now each element
* returned by the iterator is either an instance of {@link FormError} or of
* {@link FormErrorIterator}, in case the errors belong to a sub-form.
*
* You can also wrap the iterator into a {@link \RecursiveIteratorIterator} to
* flatten the recursive structure into a flat list of errors.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @since 2.5
*/
class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable
{
/**
* The prefix used for indenting nested error messages.
*
* @var string
*/
const INDENTATION = ' ';
/**
* @var FormInterface
*/
private $form;
/**
* @var Boolean
*/
private $deep;
/**
* @var Boolean
*/
private $flatten;
/**
* @var array
*/
private $elements;
/**
* Creates a new iterator.
*
* @param array $errors The iterated errors
* @param FormInterface $form The form the errors belong to
* @param Boolean $deep Whether to include the errors of child
* forms
* @param Boolean $flatten Whether to flatten the recursive list of
* errors into a flat list
*/
public function __construct(array &$errors, FormInterface $form, $deep = false, $flatten = false)
{
$this->errors = &$errors;
$this->form = $form;
$this->deep = $deep;
$this->flatten = $flatten;
$this->rewind();
}
/**
* Returns all iterated error messages as string.
*
* @return string The iterated error messages
*/
public function __toString()
{
$string = '';
foreach ($this->elements as $element) {
if ($element instanceof FormError) {
$string .= 'ERROR: '.$element->getMessage()."\n";
} else {
/** @var $element FormErrorIterator */
$string .= $element->form->getName().":\n";
$string .= self::indent((string) $element);
}
}
return $string;
}
/**
* Returns the iterated form.
*
* @return FormInterface The form whose errors are iterated by this object.
*/
public function getForm()
{
return $this->form;
}
/**
* Returns the current element of the iterator.
*
* @return FormError|FormErrorIterator An error or an iterator for nested
* errors.
*/
public function current()
{
return current($this->elements);
}
/**
* Advances the iterator to the next position.
*/
public function next()
{
next($this->elements);
}
/**
* Returns the current position of the iterator.
*
* @return integer The 0-indexed position.
*/
public function key()
{
return key($this->elements);
}
/**
* Returns whether the iterator's position is valid.
*
* @return Boolean Whether the iterator is valid.
*/
public function valid()
{
return null !== key($this->elements);
}
/**
* Sets the iterator's position to the beginning.
*
* This method detects if errors have been added to the form since the
* construction of the iterator.
*/
public function rewind()
{
$this->elements = $this->errors;
if ($this->deep) {
foreach ($this->form as $child) {
/** @var FormInterface $child */
if ($child->isSubmitted() && $child->isValid()) {
continue;
}
$iterator = $child->getErrors(true, $this->flatten);
if (0 === count($iterator)) {
continue;
}
if ($this->flatten) {
foreach ($iterator as $error) {
$this->elements[] = $error;
}
} else {
$this->elements[] = $iterator;
}
}
}
reset($this->elements);
}
/**
* Returns whether a position exists in the iterator.
*
* @param integer $position The position
*
* @return Boolean Whether that position exists
*/
public function offsetExists($position)
{
return isset($this->elements[$position]);
}
/**
* Returns the element at a position in the iterator.
*
* @param integer $position The position
*
* @return FormError|FormErrorIterator The element at the given position
*
* @throws OutOfBoundsException If the given position does not exist
*/
public function offsetGet($position)
{
if (!isset($this->elements[$position])) {
throw new OutOfBoundsException('The offset '.$position.' does not exist.');
}
return $this->elements[$position];
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function offsetSet($position, $value)
{
throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
}
/**
* Unsupported method.
*
* @throws BadMethodCallException
*/
public function offsetUnset($position)
{
throw new BadMethodCallException('The iterator doesn\'t support modification of elements.');
}
/**
* Returns whether the current element of the iterator can be recursed
* into.
*
* @return Boolean Whether the current element is an instance of this class
*/
public function hasChildren()
{
return current($this->elements) instanceof self;
}
/**
* Alias of {@link current()}.
*/
public function getChildren()
{
return current($this->elements);
}
/**
* Returns the number of elements in the iterator.
*
* Note that this is not the total number of errors, if the constructor
* parameter $deep was set to true! In that case, you should wrap the
* iterator into a {@link \RecursiveIteratorIterator} with the standard mode
* {@link \RecursiveIteratorIterator::LEAVES_ONLY} and count the result.
*
* $iterator = new \RecursiveIteratorIterator($form->getErrors(true));
* $count = count(iterator_to_array($iterator));
*
* Alternatively, set the constructor argument $flatten to true as well.
*
* $count = count($form->getErrors(true, true));
*
* @return integer The number of iterated elements
*/
public function count()
{
return count($this->elements);
}
/**
* Sets the position of the iterator.
*
* @param integer $position The new position
*
* @throws OutOfBoundsException If the position is invalid
*/
public function seek($position)
{
if (!isset($this->elements[$position])) {
throw new OutOfBoundsException('The offset '.$position.' does not exist.');
}
reset($this->elements);
while ($position !== key($this->elements)) {
next($this->elements);
}
}
/**
* Utility function for indenting multi-line strings.
*
* @param string $string The string
*
* @return string The indented string
*/
private static function indent($string)
{
return rtrim(self::INDENTATION.str_replace("\n", "\n".self::INDENTATION, $string), ' ');
}
}

View File

@ -92,11 +92,19 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
public function all();
/**
* Returns all errors.
* Returns the errors of this form.
*
* @return FormError[] An array of FormError instances that occurred during validation
* @param Boolean $deep Whether to include errors of child forms as well
* @param Boolean $flatten Whether to flatten the list of errors in case
* $deep is set to true
*
* @return FormErrorIterator An iterator over the {@link FormError}
* instances that where added to this form
*
* @since 2.5 Since version 2.5 this method returns a
* {@link FormErrorIterator} instance instead of an array
*/
public function getErrors();
public function getErrors($deep = false, $flatten = false);
/**
* Updates the form with default data.

View File

@ -819,7 +819,101 @@ class CompoundFormTest extends AbstractFormTest
$parent->add($this->form);
$parent->add($this->getBuilder('foo')->getForm());
$this->assertEquals("name:\n ERROR: Error!\nfoo:\n No errors\n", $parent->getErrorsAsString());
$this->assertSame(
"name:\n".
" ERROR: Error!\n",
$parent->getErrorsAsString()
);
}
public function testGetErrorsAsStringDeepWithIndentation()
{
$parent = $this->getBuilder()
->setCompound(true)
->setDataMapper($this->getDataMapper())
->getForm();
$this->form->addError(new FormError('Error!'));
$parent->add($this->form);
$parent->add($this->getBuilder('foo')->getForm());
$this->assertSame(
" name:\n".
" ERROR: Error!\n",
$parent->getErrorsAsString(4)
);
}
public function testGetErrors()
{
$this->form->addError($error1 = new FormError('Error 1'));
$this->form->addError($error2 = new FormError('Error 2'));
$errors = $this->form->getErrors();
$this->assertSame(
"ERROR: Error 1\n".
"ERROR: Error 2\n",
(string) $errors
);
$this->assertSame(array($error1, $error2), iterator_to_array($errors));
}
public function testGetErrorsDeep()
{
$this->form->addError($error1 = new FormError('Error 1'));
$this->form->addError($error2 = new FormError('Error 2'));
$childForm = $this->getBuilder('Child')->getForm();
$childForm->addError($nestedError = new FormError('Nested Error'));
$this->form->add($childForm);
$errors = $this->form->getErrors(true);
$this->assertSame(
"ERROR: Error 1\n".
"ERROR: Error 2\n".
"Child:\n".
" ERROR: Nested Error\n",
(string) $errors
);
$errorsAsArray = iterator_to_array($errors);
$this->assertSame($error1, $errorsAsArray[0]);
$this->assertSame($error2, $errorsAsArray[1]);
$this->assertInstanceOf('Symfony\Component\Form\FormErrorIterator', $errorsAsArray[2]);
$nestedErrorsAsArray = iterator_to_array($errorsAsArray[2]);
$this->assertCount(1, $nestedErrorsAsArray);
$this->assertSame($nestedError, $nestedErrorsAsArray[0]);
}
public function testGetErrorsDeepFlat()
{
$this->form->addError($error1 = new FormError('Error 1'));
$this->form->addError($error2 = new FormError('Error 2'));
$childForm = $this->getBuilder('Child')->getForm();
$childForm->addError($nestedError = new FormError('Nested Error'));
$this->form->add($childForm);
$errors = $this->form->getErrors(true, true);
$this->assertSame(
"ERROR: Error 1\n".
"ERROR: Error 2\n".
"ERROR: Nested Error\n",
(string) $errors
);
$this->assertSame(
array($error1, $error2, $nestedError),
iterator_to_array($errors)
);
}
// Basic cases are covered in SimpleFormTest

View File

@ -128,7 +128,7 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
$this->mapper->mapViolation($violation, $parent);
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $child->getName().' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($child->getErrors()), $child->getName().' should have an error, but has none');
$this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one');
}
@ -155,7 +155,7 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $child->getName().' should not have an error, but has one');
$this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $grandGrandChild->getErrors(), $grandGrandChild->getName().' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($grandGrandChild->getErrors()), $grandGrandChild->getName().' should have an error, but has none');
}
public function testAbortMappingIfNotSynchronized()
@ -746,17 +746,17 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
$this->mapper->mapViolation($violation, $parent);
if (self::LEVEL_0 === $target) {
$this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($parent->getErrors()), $parent->getName().' should have an error, but has none');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} elseif (self::LEVEL_1 === $target) {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($child->getErrors()), $childName.' should have an error, but has none');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} else {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($grandChild->getErrors()), $grandChildName.' should have an error, but has none');
}
}
@ -1218,17 +1218,17 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
}
if (self::LEVEL_0 === $target) {
$this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($parent->getErrors()), $parent->getName().' should have an error, but has none');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} elseif (self::LEVEL_1 === $target) {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($child->getErrors()), $childName.' should have an error, but has none');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} else {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($grandChild->getErrors()), $grandChildName.' should have an error, but has none');
}
}
@ -1400,16 +1400,16 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
if (self::LEVEL_0 === $target) {
$this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($parent->getErrors()), $parent->getName().' should have an error, but has none');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} elseif (self::LEVEL_1 === $target) {
$this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($child->getErrors()), $childName.' should have an error, but has none');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} elseif (self::LEVEL_1B === $target) {
$this->assertEquals(array($this->getFormError($violation)), $errorChild->getErrors(), $errorName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($errorChild->getErrors()), $errorName.' should have an error, but has none');
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
@ -1417,7 +1417,7 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
$this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($grandChild->getErrors()), $grandChildName.' should have an error, but has none');
}
}
@ -1462,17 +1462,17 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase
$this->mapper->mapViolation($violation, $parent);
if (self::LEVEL_0 === $target) {
$this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($parent->getErrors()), $parent->getName().' should have an error, but has none');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} elseif (self::LEVEL_1 === $target) {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($child->getErrors()), $childName.' should have an error, but has none');
$this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
} else {
$this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
$this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
$this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
$this->assertEquals(array($this->getFormError($violation)), iterator_to_array($grandChild->getErrors()), $grandChildName.' should have an error, but has none');
}
}
}

View File

@ -664,7 +664,7 @@ class SimpleFormTest extends AbstractFormTest
$this->form->addError(new FormError('Error!'));
$this->form->submit('foobar');
$this->assertSame(array(), $this->form->getErrors());
$this->assertCount(0, $this->form->getErrors());
}
public function testCreateView()