merged branch ericclemmons/1735-entity_choice_list_group_by (PR #2464)

Commits
-------

6cb7acf CS - camelCase & curly braces
d9b7abb Added EntityChoiceList test for `group_by` and invalid, deep property paths
e6554d6 Removed Closure support from group_by (PropertyPath strings only)
037933a CS - (String) renamed to (string)
7ad0f05 Added group_by test for EntityType
882482a Added group_by tests for EntityChoiceList
040e988 `EntityChoiceList` now supports grouping of entities by property path or closure
b171a6a Added `group_by` to EntityType

Discussion
----------

[Doctrine] [Form] EntityType+EntityChoiceList supports grouping choices

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: #1735

Per the discussion in #1735, `EntityType` does not immediately support grouping options, though I updated support for it in `EntityChoiceList` in fb9d951b1d.

This PR accomplishes the following:

* Adds optional `group_by` property to `EntityType` that supports either a `PropertyPath` or a `\Closure` that is evaluated on the entity choices
* Support for groups is added via the constructor in `EntityChoiceList`
* Groups are created prior to `EntityChoiceList#loadEntities` via a new `groupEntities` function
* Added tests for `EntityChoiceList`
* Added test for `EntityType` `group_by` support

*There is an alternative version that only modifies `EntityType`, but that requires the addition of `EntityType#buildView(...)`, which is messy, IMO: https://github.com/ericclemmons/symfony/compare/master...1735-entity_type_group_by*

---------------------------------------------------------------------------

by fabpot at 2011/10/25 01:48:23 -0700

ping @beberlei

---------------------------------------------------------------------------

by beberlei at 2011/10/25 03:06:05 -0700

I didnt run the tests, but generally this looks very good and is a good extension.

---------------------------------------------------------------------------

by beberlei at 2011/11/01 06:25:09 -0700

@fabpot i revewied this and it looks very good, tests all pass, i think this is a very nice addition.
This commit is contained in:
Fabien Potencier 2011-11-01 15:23:39 +01:00
commit 3f38b9c537
5 changed files with 161 additions and 2 deletions

View File

@ -81,7 +81,14 @@ class EntityChoiceList extends ArrayChoiceList
private $propertyPath;
public function __construct(EntityManager $em, $class, $property = null, $queryBuilder = null, $choices = array())
/**
* Closure or PropertyPath string on Entity to use for grouping of entities
*
* @var mixed
*/
private $groupBy;
public function __construct(EntityManager $em, $class, $property = null, $queryBuilder = null, $choices = array(), $groupBy = null)
{
// If a query builder was passed, it must be a closure or QueryBuilder
// instance
@ -102,6 +109,7 @@ class EntityChoiceList extends ArrayChoiceList
$this->queryBuilder = $queryBuilder;
$this->unitOfWork = $em->getUnitOfWork();
$this->identifier = $em->getClassMetadata($class)->getIdentifierFieldNames();
$this->groupBy = $groupBy;
// The property option defines, which property (path) is used for
// displaying entities as strings
@ -138,11 +146,39 @@ class EntityChoiceList extends ArrayChoiceList
$this->choices = array();
$this->entities = array();
if ($this->groupBy) {
$entities = $this->groupEntities($entities, $this->groupBy);
}
$this->loadEntities($entities);
return $this->choices;
}
private function groupEntities($entities, $groupBy)
{
$grouped = array();
foreach ($entities as $entity) {
// Get group name from property path
try {
$path = new PropertyPath($groupBy);
$group = (string) $path->getValue($entity);
} catch (UnexpectedTypeException $e) {
// PropertyPath cannot traverse entity
$group = null;
}
if (empty($group)) {
$grouped[] = $entity;
} else {
$grouped[$group][] = $entity;
}
}
return $grouped;
}
/**
* Convert entities into choices with support for groups
*

View File

@ -48,6 +48,7 @@ class EntityType extends AbstractType
'property' => null,
'query_builder' => null,
'choices' => array(),
'group_by' => null,
);
$options = array_replace($defaultOptions, $options);
@ -58,7 +59,8 @@ class EntityType extends AbstractType
$options['class'],
$options['property'],
$options['query_builder'],
$options['choices']
$options['choices'],
$options['group_by']
);
}

View File

@ -0,0 +1,27 @@
<?php
namespace Symfony\Tests\Bridge\Doctrine\Form\Fixtures;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
/** @Entity */
class ItemGroupEntity
{
/** @Id @Column(type="integer") */
protected $id;
/** @Column(type="string", nullable=true) */
public $name;
/** @Column(type="string", nullable=true) */
public $groupName;
public function __construct($id, $name, $groupName)
{
$this->id = $id;
$this->name = $name;
$this->groupName = $groupName;
}
}

View File

@ -12,14 +12,18 @@
namespace Symfony\Tests\Bridge\Doctrine\Form\ChoiceList;
require_once __DIR__.'/../DoctrineOrmTestCase.php';
require_once __DIR__.'/../../Fixtures/ItemGroupEntity.php';
require_once __DIR__.'/../../Fixtures/SingleIdentEntity.php';
use Symfony\Tests\Bridge\Doctrine\Form\DoctrineOrmTestCase;
use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\ItemGroupEntity;
use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
class EntityChoiceListTest extends DoctrineOrmTestCase
{
const ITEM_GROUP_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\ItemGroupEntity';
const SINGLE_IDENT_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity';
const COMPOSITE_IDENT_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\CompositeIdentEntity';
@ -113,4 +117,63 @@ class EntityChoiceListTest extends DoctrineOrmTestCase
'group2' => array(2 => 'Bar')
), $choiceList->getChoices());
}
public function testGroupBySupportsString()
{
$item1 = new ItemGroupEntity(1, 'Foo', 'Group1');
$item2 = new ItemGroupEntity(2, 'Bar', 'Group1');
$item3 = new ItemGroupEntity(3, 'Baz', 'Group2');
$item4 = new ItemGroupEntity(4, 'Boo!', null);
$this->em->persist($item1);
$this->em->persist($item2);
$this->em->persist($item3);
$this->em->persist($item4);
$choiceList = new EntityChoiceList(
$this->em,
self::ITEM_GROUP_CLASS,
'name',
null,
array(
$item1,
$item2,
$item3,
$item4,
),
'groupName'
);
$this->assertEquals(array(
'Group1' => array(1 => 'Foo', '2' => 'Bar'),
'Group2' => array(3 => 'Baz'),
'4' => 'Boo!'
), $choiceList->getChoices('choices'));
}
public function testGroupByInvalidPropertyPathReturnsFlatChoices()
{
$item1 = new ItemGroupEntity(1, 'Foo', 'Group1');
$item2 = new ItemGroupEntity(2, 'Bar', 'Group1');
$this->em->persist($item1);
$this->em->persist($item2);
$choiceList = new EntityChoiceList(
$this->em,
self::ITEM_GROUP_CLASS,
'name',
null,
array(
$item1,
$item2,
),
'groupName.child.that.does.not.exist'
);
$this->assertEquals(array(
1 => 'Foo',
2 => 'Bar'
), $choiceList->getChoices('choices'));
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Tests\Bridge\Doctrine\Form\Type;
require_once __DIR__.'/../DoctrineOrmTestCase.php';
require_once __DIR__.'/../../Fixtures/ItemGroupEntity.php';
require_once __DIR__.'/../../Fixtures/SingleIdentEntity.php';
require_once __DIR__.'/../../Fixtures/SingleStringIdentEntity.php';
require_once __DIR__.'/../../Fixtures/CompositeIdentEntity.php';
@ -20,6 +21,7 @@ require_once __DIR__.'/../../Fixtures/CompositeStringIdentEntity.php';
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Tests\Component\Form\Extension\Core\Type\TypeTestCase;
use Symfony\Tests\Bridge\Doctrine\Form\DoctrineOrmTestCase;
use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\ItemGroupEntity;
use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity;
use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleStringIdentEntity;
use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\CompositeIdentEntity;
@ -31,6 +33,7 @@ use Doctrine\Common\Collections\ArrayCollection;
class EntityTypeTest extends TypeTestCase
{
const ITEM_GROUP_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\ItemGroupEntity';
const SINGLE_IDENT_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity';
const SINGLE_STRING_IDENT_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleStringIdentEntity';
const COMPOSITE_IDENT_CLASS = 'Symfony\Tests\Bridge\Doctrine\Form\Fixtures\CompositeIdentEntity';
@ -50,6 +53,7 @@ class EntityTypeTest extends TypeTestCase
$schemaTool = new SchemaTool($this->em);
$classes = array(
$this->em->getClassMetadata(self::ITEM_GROUP_CLASS),
$this->em->getClassMetadata(self::SINGLE_IDENT_CLASS),
$this->em->getClassMetadata(self::SINGLE_STRING_IDENT_CLASS),
$this->em->getClassMetadata(self::COMPOSITE_IDENT_CLASS),
@ -461,6 +465,33 @@ class EntityTypeTest extends TypeTestCase
$this->assertEquals(2, $field->getClientData());
}
public function testGroupByChoices()
{
$item1 = new ItemGroupEntity(1, 'Foo', 'Group1');
$item2 = new ItemGroupEntity(2, 'Bar', 'Group1');
$item3 = new ItemGroupEntity(3, 'Baz', 'Group2');
$item4 = new ItemGroupEntity(4, 'Boo!', null);
$this->persist(array($item1, $item2, $item3, $item4));
$field = $this->factory->createNamed('entity', 'name', null, array(
'em' => 'default',
'class' => self::ITEM_GROUP_CLASS,
'choices' => array($item1, $item2, $item3, $item4),
'property' => 'name',
'group_by' => 'groupName',
));
$field->bind('2');
$this->assertEquals(2, $field->getClientData());
$this->assertEquals(array(
'Group1' => array(1 => 'Foo', '2' => 'Bar'),
'Group2' => array(3 => 'Baz'),
'4' => 'Boo!'
), $field->createView()->get('choices'));
}
public function testDisallowChoicesThatAreNotIncluded_choicesSingleIdentifier()
{
$entity1 = new SingleIdentEntity(1, 'Foo');