Skip to content

Commit

Permalink
[Form] Added support for virtual field groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernhard Schussek authored and fabpot committed Jan 3, 2011
1 parent 8b843e2 commit ba422e8
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 13 deletions.
25 changes: 21 additions & 4 deletions src/Symfony/Component/Form/FieldGroup.php
Expand Up @@ -13,7 +13,6 @@

use Symfony\Component\Form\Exception\AlreadyBoundException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Iterator\RecursiveFieldsWithPropertyPathIterator;

/**
* FieldGroup represents an array of widgets bind to names and values.
Expand All @@ -34,6 +33,16 @@ class FieldGroup extends Field implements \IteratorAggregate, FieldGroupInterfac
*/
protected $extraFields = array();

/**
* @inheritDoc
*/
public function __construct($key, array $options = array())
{
$this->addOption('virtual', false);

parent::__construct($key, $options);
}

/**
* Clones this group
*/
Expand Down Expand Up @@ -317,7 +326,7 @@ public function bind($taintedData)
*/
protected function updateFromObject(&$objectOrArray)
{
$iterator = new RecursiveFieldsWithPropertyPathIterator($this);
$iterator = new RecursiveFieldIterator($this);
$iterator = new \RecursiveIteratorIterator($iterator);

foreach ($iterator as $field) {
Expand All @@ -336,7 +345,7 @@ protected function updateFromObject(&$objectOrArray)
*/
protected function updateObject(&$objectOrArray)
{
$iterator = new RecursiveFieldsWithPropertyPathIterator($this);
$iterator = new RecursiveFieldIterator($this);
$iterator = new \RecursiveIteratorIterator($iterator);

foreach ($iterator as $field) {
Expand All @@ -357,6 +366,14 @@ protected function preprocessData(array $data)
return $data;
}

/**
* @inheritDoc
*/
public function isVirtual()
{
return $this->getOption('virtual');
}

/**
* Returns whether this form was bound with extra fields
*
Expand Down Expand Up @@ -407,7 +424,7 @@ public function addError(FieldError $error, PropertyPathIterator $pathIterator =
return;
}
} else if ($type === self::DATA_ERROR) {
$iterator = new RecursiveFieldsWithPropertyPathIterator($this);
$iterator = new RecursiveFieldIterator($this);
$iterator = new \RecursiveIteratorIterator($iterator);

foreach ($iterator as $field) {
Expand Down
23 changes: 23 additions & 0 deletions src/Symfony/Component/Form/FieldGroupInterface.php
Expand Up @@ -18,4 +18,27 @@
*/
interface FieldGroupInterface extends FieldInterface, \ArrayAccess, \Traversable, \Countable
{
/**
* Returns whether this field group is virtual
*
* Virtual field groups are skipped when mapping property paths of a form
* tree to an object.
*
* Example:
*
* <code>
* $group = new FieldGroup('address');
* $group->add(new TextField('street'));
* $group->add(new TextField('postal_code'));
* $form->add($group);
* </code>
*
* If $group is non-virtual, the fields "street" and "postal_code"
* are mapped to the property paths "address.street" and
* "address.postal_code". If $group is virtual though, the fields are
* mapped directly to "street" and "postal_code".
*
* @return boolean Whether the group is virtual
*/
public function isVirtual();
}
@@ -1,6 +1,6 @@
<?php

namespace Symfony\Component\Form\Iterator;
namespace Symfony\Component\Form;

/*
* This file is part of the Symfony framework.
Expand All @@ -11,9 +11,15 @@
* with this source code in the file LICENSE.
*/

use Symfony\Component\Form\FieldGroupInterface;

class RecursiveFieldsWithPropertyPathIterator extends \IteratorIterator implements \RecursiveIterator
/**
* Iterator that traverses fields of a field group
*
* If the iterator encounters a virtual field group, it enters the field
* group and traverses its children as well.
*
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
*/
class RecursiveFieldIterator extends \IteratorIterator implements \RecursiveIterator
{
public function __construct(FieldGroupInterface $group)
{
Expand All @@ -27,6 +33,7 @@ public function getChildren()

public function hasChildren()
{
return $this->current() instanceof FieldGroupInterface && $this->current()->getPropertyPath() === null;
return $this->current() instanceof FieldGroupInterface
&& $this->current()->isVirtual();
}
}
58 changes: 54 additions & 4 deletions tests/Symfony/Tests/Component/Form/FieldGroupTest.php
Expand Up @@ -322,7 +322,7 @@ public function testAddErrorMapsDataValidationErrorsOntoNestedFields()
$group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR);
}

public function testAddErrorMapsErrorsOntoFieldsInAnonymousGroups()
public function testAddErrorMapsErrorsOntoFieldsInVirtualGroups()
{
$error = new FieldError('Message');

Expand All @@ -339,9 +339,9 @@ public function testAddErrorMapsErrorsOntoFieldsInAnonymousGroups()
->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR));

$group = new TestFieldGroup('author');
$group2 = new TestFieldGroup('anonymous', array('property_path' => null));
$group2->add($field);
$group->add($group2);
$nestedGroup = new TestFieldGroup('nested', array('virtual' => true));
$nestedGroup->add($field);
$group->add($nestedGroup);

$path = new PropertyPath('address');

Expand Down Expand Up @@ -469,6 +469,56 @@ public function testSetDataUpdatesAllFieldsFromTransformedData()
$group->setData($originalAuthor);
}

/**
* The use case for this test are groups whose fields should be mapped
* directly onto properties of the form's object.
*
* Example:
*
* <code>
* $dateRangeField = new FieldGroup('dateRange');
* $dateRangeField->add(new DateField('startDate'));
* $dateRangeField->add(new DateField('endDate'));
* $form->add($dateRangeField);
* </code>
*
* If $dateRangeField is not virtual, the property "dateRange" must be
* present on the form's object. In this property, an object or array
* with the properties "startDate" and "endDate" is expected.
*
* If $dateRangeField is virtual though, it's children are mapped directly
* onto the properties "startDate" and "endDate" of the form's object.
*/
public function testSetDataSkipsVirtualFieldGroups()
{
$author = new Author();
$author->firstName = 'Foo';

$group = new TestFieldGroup('author');
$nestedGroup = new TestFieldGroup('personal_data', array(
'virtual' => true,
));

// both fields are in the nested group but receive the object of the
// top-level group because the nested group is virtual
$field = $this->createMockField('firstName');
$field->expects($this->once())
->method('updateFromProperty')
->with($this->equalTo($author));

$nestedGroup->add($field);

$field = $this->createMockField('lastName');
$field->expects($this->once())
->method('updateFromProperty')
->with($this->equalTo($author));

$nestedGroup->add($field);

$group->add($nestedGroup);
$group->setData($author);
}

public function testSetDataThrowsAnExceptionIfArgumentIsNotObjectOrArray()
{
$group = new TestFieldGroup('author');
Expand Down

0 comments on commit ba422e8

Please sign in to comment.