Skip to content

Commit ba422e8

Browse files
Bernhard Schussekfabpot
authored andcommitted
[Form] Added support for virtual field groups
1 parent 8b843e2 commit ba422e8

File tree

4 files changed

+110
-13
lines changed

4 files changed

+110
-13
lines changed

src/Symfony/Component/Form/FieldGroup.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Component\Form\Exception\AlreadyBoundException;
1515
use Symfony\Component\Form\Exception\UnexpectedTypeException;
16-
use Symfony\Component\Form\Iterator\RecursiveFieldsWithPropertyPathIterator;
1716

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

36+
/**
37+
* @inheritDoc
38+
*/
39+
public function __construct($key, array $options = array())
40+
{
41+
$this->addOption('virtual', false);
42+
43+
parent::__construct($key, $options);
44+
}
45+
3746
/**
3847
* Clones this group
3948
*/
@@ -317,7 +326,7 @@ public function bind($taintedData)
317326
*/
318327
protected function updateFromObject(&$objectOrArray)
319328
{
320-
$iterator = new RecursiveFieldsWithPropertyPathIterator($this);
329+
$iterator = new RecursiveFieldIterator($this);
321330
$iterator = new \RecursiveIteratorIterator($iterator);
322331

323332
foreach ($iterator as $field) {
@@ -336,7 +345,7 @@ protected function updateFromObject(&$objectOrArray)
336345
*/
337346
protected function updateObject(&$objectOrArray)
338347
{
339-
$iterator = new RecursiveFieldsWithPropertyPathIterator($this);
348+
$iterator = new RecursiveFieldIterator($this);
340349
$iterator = new \RecursiveIteratorIterator($iterator);
341350

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

369+
/**
370+
* @inheritDoc
371+
*/
372+
public function isVirtual()
373+
{
374+
return $this->getOption('virtual');
375+
}
376+
360377
/**
361378
* Returns whether this form was bound with extra fields
362379
*
@@ -407,7 +424,7 @@ public function addError(FieldError $error, PropertyPathIterator $pathIterator =
407424
return;
408425
}
409426
} else if ($type === self::DATA_ERROR) {
410-
$iterator = new RecursiveFieldsWithPropertyPathIterator($this);
427+
$iterator = new RecursiveFieldIterator($this);
411428
$iterator = new \RecursiveIteratorIterator($iterator);
412429

413430
foreach ($iterator as $field) {

src/Symfony/Component/Form/FieldGroupInterface.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,27 @@
1818
*/
1919
interface FieldGroupInterface extends FieldInterface, \ArrayAccess, \Traversable, \Countable
2020
{
21+
/**
22+
* Returns whether this field group is virtual
23+
*
24+
* Virtual field groups are skipped when mapping property paths of a form
25+
* tree to an object.
26+
*
27+
* Example:
28+
*
29+
* <code>
30+
* $group = new FieldGroup('address');
31+
* $group->add(new TextField('street'));
32+
* $group->add(new TextField('postal_code'));
33+
* $form->add($group);
34+
* </code>
35+
*
36+
* If $group is non-virtual, the fields "street" and "postal_code"
37+
* are mapped to the property paths "address.street" and
38+
* "address.postal_code". If $group is virtual though, the fields are
39+
* mapped directly to "street" and "postal_code".
40+
*
41+
* @return boolean Whether the group is virtual
42+
*/
43+
public function isVirtual();
2144
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace Symfony\Component\Form\Iterator;
3+
namespace Symfony\Component\Form;
44

55
/*
66
* This file is part of the Symfony framework.
@@ -11,9 +11,15 @@
1111
* with this source code in the file LICENSE.
1212
*/
1313

14-
use Symfony\Component\Form\FieldGroupInterface;
15-
16-
class RecursiveFieldsWithPropertyPathIterator extends \IteratorIterator implements \RecursiveIterator
14+
/**
15+
* Iterator that traverses fields of a field group
16+
*
17+
* If the iterator encounters a virtual field group, it enters the field
18+
* group and traverses its children as well.
19+
*
20+
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
21+
*/
22+
class RecursiveFieldIterator extends \IteratorIterator implements \RecursiveIterator
1723
{
1824
public function __construct(FieldGroupInterface $group)
1925
{
@@ -27,6 +33,7 @@ public function getChildren()
2733

2834
public function hasChildren()
2935
{
30-
return $this->current() instanceof FieldGroupInterface && $this->current()->getPropertyPath() === null;
36+
return $this->current() instanceof FieldGroupInterface
37+
&& $this->current()->isVirtual();
3138
}
3239
}

tests/Symfony/Tests/Component/Form/FieldGroupTest.php

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ public function testAddErrorMapsDataValidationErrorsOntoNestedFields()
322322
$group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR);
323323
}
324324

325-
public function testAddErrorMapsErrorsOntoFieldsInAnonymousGroups()
325+
public function testAddErrorMapsErrorsOntoFieldsInVirtualGroups()
326326
{
327327
$error = new FieldError('Message');
328328

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

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

346346
$path = new PropertyPath('address');
347347

@@ -469,6 +469,56 @@ public function testSetDataUpdatesAllFieldsFromTransformedData()
469469
$group->setData($originalAuthor);
470470
}
471471

472+
/**
473+
* The use case for this test are groups whose fields should be mapped
474+
* directly onto properties of the form's object.
475+
*
476+
* Example:
477+
*
478+
* <code>
479+
* $dateRangeField = new FieldGroup('dateRange');
480+
* $dateRangeField->add(new DateField('startDate'));
481+
* $dateRangeField->add(new DateField('endDate'));
482+
* $form->add($dateRangeField);
483+
* </code>
484+
*
485+
* If $dateRangeField is not virtual, the property "dateRange" must be
486+
* present on the form's object. In this property, an object or array
487+
* with the properties "startDate" and "endDate" is expected.
488+
*
489+
* If $dateRangeField is virtual though, it's children are mapped directly
490+
* onto the properties "startDate" and "endDate" of the form's object.
491+
*/
492+
public function testSetDataSkipsVirtualFieldGroups()
493+
{
494+
$author = new Author();
495+
$author->firstName = 'Foo';
496+
497+
$group = new TestFieldGroup('author');
498+
$nestedGroup = new TestFieldGroup('personal_data', array(
499+
'virtual' => true,
500+
));
501+
502+
// both fields are in the nested group but receive the object of the
503+
// top-level group because the nested group is virtual
504+
$field = $this->createMockField('firstName');
505+
$field->expects($this->once())
506+
->method('updateFromProperty')
507+
->with($this->equalTo($author));
508+
509+
$nestedGroup->add($field);
510+
511+
$field = $this->createMockField('lastName');
512+
$field->expects($this->once())
513+
->method('updateFromProperty')
514+
->with($this->equalTo($author));
515+
516+
$nestedGroup->add($field);
517+
518+
$group->add($nestedGroup);
519+
$group->setData($author);
520+
}
521+
472522
public function testSetDataThrowsAnExceptionIfArgumentIsNotObjectOrArray()
473523
{
474524
$group = new TestFieldGroup('author');

0 commit comments

Comments
 (0)