[DoctrineBundle][Form] Implemented EntityChoiceField

This commit is contained in:
Bernhard Schussek 2011-01-27 15:16:11 +01:00
parent 46d900682f
commit 347c069e8d
12 changed files with 1166 additions and 408 deletions

View File

@ -1,88 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\DoctrineBundle\Form\ValueTransformer;
use Symfony\Component\Form\ValueTransformer\BaseValueTransformer;
use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
/**
* Transforms a Collection into a Choice field used for Multiple Select fields or checkbox groups.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class CollectionToChoiceTransformer extends BaseValueTransformer
{
protected function configure()
{
$this->addRequiredOption('em');
$this->addRequiredOption('className');
parent::configure();
}
/**
* @param array $ids
* @param Collection $collection
*/
public function reverseTransform($ids, $collection)
{
if (count($ids) == 0) {
// don't check for collection count, a straight clear doesnt initialize the collection
$collection->clear();
return $collection;
}
$em = $this->getOption('em');
$metadata = $em->getClassMetadata($this->getOption('className'));
$reflField = $metadata->getReflectionProperty($metadata->identifier[0]);
foreach ($collection AS $object) {
$key = array_search($reflField->getValue($object), $ids);
if (false === $key) {
$collection->removeElement($object);
} else {
unset($ids[$key]);
}
}
// @todo: This can be greatly optimized into a single SELECT .. WHERE id IN () query.
foreach ($ids AS $id) {
$entity = $em->find($this->getOption('className'), $id);
if (!$entity) {
throw new TransformationFailedException("Selected entity of type '" . $this->getOption('className') . "' by id '" . $id . "' which is not present in the database.");
}
$collection->add($entity);
}
return $collection;
}
/**
* @param Collection $value
*/
public function transform($value)
{
if (null === $value) {
return array();
}
$metadata = $this->getOption('em')->getClassMetadata($this->getOption('className'));
$reflField = $metadata->getReflectionProperty($metadata->identifier[0]);
$ids = array();
foreach ($value AS $object) {
$ids[] = $reflField->getValue($object);
}
return $ids;
}
}

View File

@ -1,64 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\DoctrineBundle\Form\ValueTransformer;
use Symfony\Component\Form\ValueTransformer\BaseValueTransformer;
use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
/**
* Transforms a Doctrine Entity into its identifier value and back.
*
* This only works with single-field primary key fields.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
class EntityToIDTransformer extends BaseValueTransformer
{
protected function configure()
{
$this->addRequiredOption('em');
$this->addRequiredOption('className');
parent::configure();
}
/**
* Reverse Transforming the selected id value to an Doctrine Entity.
*
* This handles NULL, the EntityManager#find method returns null if no entity was found.
*
* @param int|string $newId
* @param object $oldEntity
* @return object
*/
public function reverseTransform($newId, $oldEntity)
{
if (empty($newId)) {
return null;
}
return $this->getOption('em')->find($this->getOption('className'), $newId);
}
/**
* @param object $entity
* @return int|string
*/
public function transform($entity)
{
if (empty($entity)) {
return 0;
}
return current( $this->getOption('em')->getUnitOfWork()->getEntityIdentifier($entity) );
}
}

View File

@ -1,141 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\CollectionToChoiceTransformer;
class CollectionToChoiceTransformerTest extends \Symfony\Bundle\DoctrineBundle\Tests\TestCase
{
/**
* @var EntityManager
*/
private $em;
protected function setUp()
{
parent::setUp();
$this->em = $this->createTestEntityManager();
$schemaTool = new SchemaTool($this->em);
$classes = array($this->em->getClassMetadata('Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag'));
try {
$schemaTool->dropSchema($classes);
} catch(\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch(\Exception $e) {
}
}
public function testCreateWithoutEntityManagerThrowsException()
{
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
$transformer = new CollectionToChoiceTransformer(array("className" => "Tag"));
}
public function testCreateWithoutClassNameThrowsException()
{
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
$transformer = new CollectionToChoiceTransformer(array("em" => $this->em));
}
public function createTransformer()
{
return new CollectionToChoiceTransformer(array(
"em" => $this->em,
"className" => 'Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag'
));
}
public function testTransformEmpty()
{
$transformer = $this->createTransformer();
$ids = $transformer->transform(new ArrayCollection());
$this->assertEquals(array(), $ids);
}
public function testTransformNull()
{
$transformer = $this->createTransformer();
$this->assertEquals(array(), $transformer->transform(null));
}
public function createTagCollection()
{
$tags = new ArrayCollection();
$tags->add(new Tag("foo"));
$tags->add(new Tag("bar"));
foreach ($tags AS $tag) {
$this->em->persist($tag);
}
$this->em->flush();
$this->em->clear();
return $tags;
}
public function testTransform()
{
$transformer = $this->createTransformer();
$ids = $transformer->transform($this->createTagCollection());
$this->assertEquals(array(1, 2), $ids);
}
public function testReverseTransformEmpty()
{
$transformer = $this->createTransformer();
$col = new ArrayCollection();
$newCol = $transformer->reverseTransform(array(), $col);
$this->assertSame($col, $newCol, "Collection is an expensive object, it should be re-used.");
$this->assertEquals(0, count($newCol));
}
public function testReverseTransformEmptyClearsCollection()
{
$transformer = $this->createTransformer();
$newCol = $transformer->reverseTransform(array(), $this->createTagCollection());
$this->assertEquals(0, count($newCol));
}
public function testReverseTransformFetchFromEntityManager()
{
$transformer = $this->createTransformer();
$col = new ArrayCollection();
$tags = $this->createTagCollection();
$newCol = $transformer->reverseTransform(array(1, 2), $col);
$this->assertEquals(2, count($newCol));
}
public function testReverseTransformRemoveMissingFromCollection()
{
$transformer = $this->createTransformer();
$tags = $this->createTagCollection();
$newCol = $transformer->reverseTransform(array(1), $tags);
$this->assertEquals(1, count($newCol));
$this->assertFalse($newCol->contains($this->em->find('Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag', 2)));
}
}

View File

@ -1,100 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\EntityToIDTransformer;
class EntityToIDTransformerTest extends \Symfony\Bundle\DoctrineBundle\Tests\TestCase
{
/**
* @var EntityManager
*/
private $em;
protected function setUp()
{
parent::setUp();
$this->em = $this->createTestEntityManager();
$schemaTool = new SchemaTool($this->em);
$classes = array($this->em->getClassMetadata('Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag'));
try {
$schemaTool->dropSchema($classes);
} catch(\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch(\Exception $e) {
}
}
public function testRequiredEntityManager()
{
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
$transformer = new EntityToIDTransformer(array('className' => 'Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag'));
}
public function testRequiredClassName()
{
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
$transformer = new EntityToIDTransformer(array('em' => $this->em));
}
public function createTransformer()
{
$transformer = new EntityToIDTransformer(array(
'em' => $this->em,
'className' => 'Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag'
));
return $transformer;
}
public function testTranformEmptyValueReturnsNull()
{
$transformer = $this->createTransformer();
$this->assertEquals(0, $transformer->transform(null));
$this->assertEquals(0, $transformer->transform(""));
$this->assertEquals(0, $transformer->transform(0));
}
public function testTransform()
{
$transformer = $this->createTransformer();
$tag = new Tag("name");
$this->em->persist($tag);
$this->em->flush();
$this->assertEquals(1, $transformer->transform($tag));
}
public function testReverseTransformEmptyValue()
{
$transformer = $this->createTransformer();
$this->assertNull($transformer->reverseTransform(0, null));
}
public function testReverseTransform()
{
$transformer = $this->createTransformer();
$tag = new Tag("name");
$this->em->persist($tag);
$this->em->flush();
$this->assertSame($tag, $transformer->reverseTransform(1, null));
}
}

View File

@ -119,6 +119,10 @@ class ChoiceField extends HybridField
{
if (!$this->choices) {
$this->choices = $this->getInitializedChoices();
if (!$this->isRequired()) {
$this->choices = array('' => $this->getOption('empty_value')) + $this->choices;
}
}
}
@ -130,15 +134,10 @@ class ChoiceField extends HybridField
$choices = $choices->__invoke();
}
// TESTME
if (!is_array($choices)) {
throw new InvalidOptionsException('The "choices" option must be an array or a closure returning an array', array('choices'));
}
if (!$this->isRequired()) {
$choices = array_merge(array('' => $this->getOption('empty_value')), $choices);
}
return $choices;
}

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\DoctrineBundle\Form\ValueTransformer;
namespace Symfony\Component\Form\Extension\Doctrine;
use Symfony\Component\Form\ValueTransformer\BaseValueTransformer;
use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
@ -33,6 +33,7 @@ use Doctrine\Common\Collections\Collection;
* @todo Refactor to make 'fieldName' optional (identifier).
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
*/
class CollectionToStringTransformer extends BaseValueTransformer
{

View File

@ -0,0 +1,504 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Doctrine;
use Symfony\Component\Form\ChoiceField;
use Symfony\Component\Form\PropertyPath;
use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\InvalidOptionsException;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\NoResultException;
/**
* A field for selecting one or more from a list of Doctrine 2 entities
*
* You at least have to pass the entity manager and the entity class in the
* options "em" and "class".
*
* <code>
* $form->add(new EntityChoiceField('tags', array(
* 'em' => $em,
* 'class' => 'Application\Entity\Tag',
* )));
* </code>
*
* Additionally to the options in ChoiceField, the following options are
* available:
*
* * em: The entity manager. Required.
* * class: The class of the selectable entities. Required.
* * property: The property displayed as value of the choices. If this
* option is not available, the field will try to convert
* objects into strings using __toString().
* * query_builder: The query builder for fetching the selectable entities.
* You can also pass a closure that receives the repository
* as single argument and returns a query builder.
*
* The following sample outlines the use of the "query_builder" option
* with closures.
*
* <code>
* $form->add(new EntityChoiceField('tags', array(
* 'em' => $em,
* 'class' => 'Application\Entity\Tag',
* 'query_builder' => function ($repository) {
* return $repository->createQueryBuilder('t')->where('t.enabled = 1');
* },
* )));
* </code>
*
* @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
*/
class EntityChoiceField extends ChoiceField
{
/**
* The entities from which the user can choose
*
* This array is either indexed by ID (if the ID is a single field)
* or by key in the choices array (if the ID consists of multiple fields)
*
* This property is initialized by initializeChoices(). It should only
* be accessed through getEntity() and getEntities().
*
* @var Collection
*/
protected $entities = null;
/**
* Contains the query builder that builds the query for fetching the
* entities
*
* This property should only be accessed through getQueryBuilder().
*
* @var Doctrine\ORM\QueryBuilder
*/
protected $queryBuilder = null;
/**
* The fields of which the identifier of the underlying class consists
*
* This property should only be accessed through getIdentifierFields().
*
* @var array
*/
protected $identifier = array();
/**
* A cache for \ReflectionProperty instances for the underlying class
*
* This property should only be accessed through getReflProperty().
*
* @var array
*/
protected $reflProperties = array();
/**
* A cache for the UnitOfWork instance of Doctrine
*
* @var Doctrine\ORM\UnitOfWork
*/
protected $unitOfWork = null;
/**
* {@inheritDoc}
*/
protected function configure()
{
$this->addRequiredOption('em');
$this->addRequiredOption('class');
$this->addOption('property');
$this->addOption('query_builder');
// Override option - it is not required for this subclass
$this->addOption('choices', array());
parent::configure();
// The entities can be passed directly in the "choices" option.
// In this case, initializing the entity cache is a cheap operation
// so do it now!
if (is_array($this->getOption('choices')) && count($this->getOption('choices')) > 0) {
$this->initializeChoices();
}
// If a query builder was passed, it must be a closure or QueryBuilder
// instance
if ($qb = $this->getOption('query_builder')) {
if (!($qb instanceof QueryBuilder || $qb instanceof \Closure)) {
throw new InvalidOptionsException(
'The option "query_builder" most contain a closure or a QueryBuilder instance',
array('query_builder'));
}
}
}
/**
* Returns the query builder instance for the choices of this field
*
* @return Doctrine\ORM\QueryBuilder The query builder
* @throws InvalidOptionsException When the query builder was passed as
* closure and that closure does not
* return a QueryBuilder instance
*/
protected function getQueryBuilder()
{
if (!$this->getOption('query_builder')) {
return null;
}
if (!$this->queryBuilder) {
$qb = $this->getOption('query_builder');
if ($qb instanceof \Closure) {
$class = $this->getOption('class');
$em = $this->getOption('em');
$qb = $qb($em->getRepository($class));
if (!$qb instanceof QueryBuilder) {
throw new InvalidOptionsException(
'The closure in the option "query_builder" should return a QueryBuilder instance',
array('query_builder'));
}
}
$this->queryBuilder = $qb;
}
return $this->queryBuilder;
}
/**
* Returns the unit of work of the entity manager
*
* This object is cached for faster lookups.
*
* @return Doctrine\ORM\UnitOfWork The unit of work
*/
protected function getUnitOfWork()
{
if (!$this->unitOfWork) {
$this->unitOfWork = $this->getOption('em')->getUnitOfWork();
}
return $this->unitOfWork;
}
/**
* Initializes the choices and returns them
*
* The choices are generated from the entities. If the entities have a
* composite identifier, the choices are indexed using ascending integers.
* Otherwise the identifiers are used as indices.
*
* If the entities were passed in the "choices" option, this method
* does not have any significant overhead. Otherwise, if a query builder
* was passed in the "query_builder" option, this builder is now used
* to construct a query which is executed. In the last case, all entities
* for the underlying class are fetched from the repository.
*
* If the option "property" was passed, the property path in that option
* is used as option values. Otherwise this method tries to convert
* objects to strings using __toString().
*
* @return array An array of choices
*/
protected function getInitializedChoices()
{
if ($this->getOption('choices')) {
$entities = parent::getInitializedChoices();
} else if ($qb = $this->getQueryBuilder()) {
$entities = $qb->getQuery()->execute();
} else {
$class = $this->getOption('class');
$em = $this->getOption('em');
$entities = $em->getRepository($class)->findAll();
}
$propertyPath = null;
$choices = array();
$this->entities = array();
// The propery option defines, which property (path) is used for
// displaying entities as strings
if ($this->getOption('property')) {
$propertyPath = new PropertyPath($this->getOption('property'));
}
foreach ($entities as $key => $entity) {
if ($propertyPath) {
// If the property option was given, use it
$value = $propertyPath->getValue($entity);
} else {
// Otherwise expect a __toString() method in the entity
$value = (string)$entity;
}
if (count($this->getIdentifierFields()) > 1) {
// When the identifier consists of multiple field, use
// naturally ordered keys to refer to the choices
$choices[$key] = $value;
$this->entities[$key] = $entity;
} else {
// When the identifier is a single field, index choices by
// entity ID for performance reasons
$id = current($this->getIdentifierValues($entity));
$choices[$id] = $value;
$this->entities[$id] = $entity;
}
}
return $choices;
}
/**
* Returns the according entities for the choices
*
* If the choices were not initialized, they are initialized now. This
* is an expensive operation, except if the entities were passed in the
* "choices" option.
*
* @return array An array of entities
*/
protected function getEntities()
{
if (!$this->entities) {
// indirectly initializes the entities property
$this->initializeChoices();
}
return $this->entities;
}
/**
* Returns the entity for the given key
*
* If the underlying entities have composite identifiers, the choices
* are intialized. The key is expected to be the index in the choices
* array in this case.
*
* If they have single identifiers, they are either fetched from the
* internal entity cache (if filled) or loaded from the database.
*
* @param string $key The choice key (for entities with composite
* identifiers) or entity ID (for entities with single
* identifiers)
* @return object The matching entity
*/
protected function getEntity($key)
{
$id = $this->getIdentifierFields();
if (count($id) > 1) {
// $key is a collection index
$entities = $this->getEntities();
return $entities[$key];
} else if ($this->entities) {
return $this->entities[$key];
} else if ($qb = $this->getQueryBuilder()) {
// should we clone the builder?
$alias = $qb->getRootAlias();
$where = $qb->expr()->eq($alias.'.'.current($id), $key);
return $qb->andWhere($where)->getQuery()->getSingleResult();
}
return $this->getOption('em')->find($this->getOption('class'), $key);
}
/**
* Returns the \ReflectionProperty instance for a property of the
* underlying class
*
* @param string $property The name of the property
* @return \ReflectionProperty The reflection instsance
*/
protected function getReflProperty($property)
{
if (!isset($this->reflProperties[$property])) {
$this->reflProperties[$property] = new \ReflectionProperty($this->getOption('class'), $field);
$this->reflProperties[$property]->setAccessible(true);
}
return $this->reflProperties[$property];
}
/**
* Returns the fields included in the identifier of the underlying class
*
* @return array An array of field names
*/
protected function getIdentifierFields()
{
if (!$this->identifier) {
$metadata = $this->getOption('em')->getClassMetadata($this->getOption('class'));
$this->identifier = $metadata->getIdentifierFieldNames();
}
return $this->identifier;
}
/**
* Returns the values of the identifier fields of an entity
*
* Doctrine must know about this entity, that is, the entity must already
* be persisted or added to the identity map before. Otherwise an
* exception is thrown.
*
* @param object $entity The entity for which to get the identifier
* @throws FormException If the entity does not exist in Doctrine's
* identity map
*/
protected function getIdentifierValues($entity)
{
if (!$this->getUnitOfWork()->isInIdentityMap($entity)) {
throw new FormException('Entities passed to the choice field must be managed');
}
return $this->getUnitOfWork()->getEntityIdentifier($entity);
}
/**
* Merges the selected and deselected entities into the collection passed
* when calling setData()
*
* @see parent::processData()
*/
protected function processData($data)
{
// reuse the existing collection to optimize for Doctrine
if ($data instanceof Collection) {
$currentData = $this->getData();
if (!$currentData) {
$currentData = $data;
} else if (count($data) === 0) {
$currentData->clear();
} else {
// merge $data into $currentData
foreach ($currentData as $entity) {
if (!$data->contains($entity)) {
$currentData->removeElement($entity);
} else {
$data->removeElement($entity);
}
}
foreach ($data as $entity) {
$currentData->add($entity);
}
}
return $currentData;
}
return $data;
}
/**
* Transforms choice keys into entities
*
* @param mixed $keyOrKeys An array of keys, a single key or NULL
* @return Collection|object A collection of entities, a single entity
* or NULL
*/
protected function reverseTransform($keyOrKeys)
{
$keyOrKeys = parent::reverseTransform($keyOrKeys);
if (null === $keyOrKeys) {
return $this->getOption('multiple') ? new ArrayCollection() : null;
}
$notFound = array();
if (count($this->getIdentifierFields()) > 1) {
$notFound = array_diff((array)$keyOrKeys, array_keys($this->getEntities()));
} else if ($this->entities) {
$notFound = array_diff((array)$keyOrKeys, array_keys($this->entities));
}
if (0 === count($notFound)) {
if (is_array($keyOrKeys)) {
$result = new ArrayCollection();
// optimize this into a SELECT WHERE IN query
foreach ($keyOrKeys as $key) {
try {
$result->add($this->getEntity($key));
} catch (NoResultException $e) {
$notFound[] = $key;
}
}
} else {
try {
$result = $this->getEntity($keyOrKeys);
} catch (NoResultException $e) {
$notFound[] = $keyOrKeys;
}
}
}
if (count($notFound) > 0) {
throw new TransformationFailedException('The entities with keys "%s" could not be found', implode('", "', $notFound));
}
return $result;
}
/**
* Transforms entities into choice keys
*
* @param Collection|object A collection of entities, a single entity or
* NULL
* @return mixed An array of choice keys, a single key or
* NULL
*/
protected function transform($collectionOrEntity)
{
if (null === $collectionOrEntity) {
return $this->getOption('multiple') ? array() : '';
}
if (count($this->identifier) > 1) {
// load all choices
$availableEntities = $this->getEntities();
if ($collectionOrEntity instanceof Collection) {
$result = array();
foreach ($collectionOrEntity as $entity) {
// identify choices by their collection key
$key = array_search($entity, $availableEntities);
$result[] = $key;
}
} else {
$result = array_search($collectionOrEntity, $availableEntities);
}
} else {
if ($collectionOrEntity instanceof Collection) {
$result = array();
foreach ($collectionOrEntity as $entity) {
$result[] = current($this->getIdentifierValues($entity));
}
} else {
$result = current($this->getIdentifierValues($collectionOrEntity));
}
}
return parent::transform($result);
}
}

View File

@ -9,13 +9,15 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer;
namespace Symfony\Tests\Component\Form\Extension\Doctrine;
use Symfony\Bundle\DoctrineBundle\Form\ValueTransformer\CollectionToStringTransformer;
require_once __DIR__.'/TestCase.php';
use Symfony\Component\Form\Extension\Doctrine\CollectionToStringTransformer;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Tools\SchemaTool;
class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\Tests\TestCase
class CollectionToStringTransformerTest extends TestCase
{
/**
* @var EntityManager
@ -28,7 +30,7 @@ class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\T
$this->em = $this->createTestEntityManager();
$schemaTool = new SchemaTool($this->em);
$classes = array($this->em->getClassMetadata('Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag'));
$classes = array($this->em->getClassMetadata(__NAMESPACE__.'\Tag'));
try {
$schemaTool->dropSchema($classes);
} catch(\Exception $e) {
@ -45,7 +47,7 @@ class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\T
{
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
$transformer = new CollectionToStringTransformer(array(
'class_name' => 'Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag',
'class_name' => __NAMESPACE__.'\Tag',
'field_name' => 'name',
));
}
@ -63,7 +65,7 @@ class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\T
{
$this->setExpectedException('Symfony\Component\Form\Exception\MissingOptionsException');
$transformer = new CollectionToStringTransformer(array(
'class_name' => 'Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag',
'class_name' => __NAMESPACE__.'\Tag',
'em' => $this->em,
));
}
@ -71,7 +73,7 @@ class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\T
public function createTransformer()
{
$transformer = new CollectionToStringTransformer(array(
'class_name' => 'Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag',
'class_name' => __NAMESPACE__.'\Tag',
'field_name' => 'name',
'em' => $this->em,
'create_instance_callback' => function($tagName) {
@ -99,7 +101,7 @@ class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\T
$tags = new ArrayCollection();
$tags->add(new Tag("foo"));
$tags->add(new Tag("bar"));
$this->assertEquals("foo,bar", $transformer->transform($tags));
}
@ -187,7 +189,7 @@ class CollectionToStringTransformerTest extends \Symfony\Bundle\DoctrineBundle\T
$this->assertSame($this->em, $transformer->getOption('em'));
$this->assertEquals(1, count($this->em->getRepository('Symfony\Bundle\DoctrineBundle\Tests\Form\ValueTransformer\Tag')->findAll()));
$this->assertEquals(1, count($this->em->getRepository(__NAMESPACE__.'\Tag')->findAll()));
}
/**

View File

@ -0,0 +1,556 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Form\Extension\Doctrine;
require_once __DIR__.'/TestCase.php';
require_once __DIR__.'/Fixtures/SingleIdentEntity.php';
require_once __DIR__.'/Fixtures/CompositeIdentEntity.php';
use Symfony\Component\Form\Extension\Doctrine\EntityChoiceField;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Tests\Component\Form\Extension\Doctrine\Fixtures\SingleIdentEntity;
use Symfony\Tests\Component\Form\Extension\Doctrine\Fixtures\CompositeIdentEntity;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Common\Collections\ArrayCollection;
class EntityChoiceFieldTest extends TestCase
{
const SINGLE_IDENT_CLASS = 'Symfony\Tests\Component\Form\Extension\Doctrine\Fixtures\SingleIdentEntity';
const COMPOSITE_IDENT_CLASS = 'Symfony\Tests\Component\Form\Extension\Doctrine\Fixtures\CompositeIdentEntity';
/**
* @var EntityManager
*/
private $em;
protected function setUp()
{
parent::setUp();
$this->em = $this->createTestEntityManager();
$schemaTool = new SchemaTool($this->em);
$classes = array(
$this->em->getClassMetadata(self::SINGLE_IDENT_CLASS),
$this->em->getClassMetadata(self::COMPOSITE_IDENT_CLASS),
);
try {
$schemaTool->dropSchema($classes);
} catch(\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch(\Exception $e) {
}
}
protected function persist(array $entities)
{
foreach ($entities as $entity) {
$this->em->persist($entity);
}
$this->em->flush();
// no clear, because entities managed by the choice field must
// be managed!
}
public function testNonRequiredContainsEmptyField()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$this->persist(array($entity1, $entity2));
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name'
));
$this->assertEquals(array('' => '', 1 => 'Foo', 2 => 'Bar'), $field->getOtherChoices());
}
// public function testSetDataToUninitializedEntityWithNonRequired()
// {
// $entity1 = new SingleIdentEntity(1, 'Foo');
// $entity2 = new SingleIdentEntity(2, 'Bar');
//
// $this->persist(array($entity1, $entity2));
//
// $field = new EntityChoiceField('name', array(
// 'em' => $this->em,
// 'class' => self::SINGLE_IDENT_CLASS,
// 'required' => false,
// 'property' => 'name'
// ));
//
// $this->assertEquals(array('' => '', 1 => 'Foo', 2 => 'Bar'), $field->getOtherChoices());
//
// }
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
*/
public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure()
{
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => new \stdClass(),
));
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidOptionsException
*/
public function testConfigureQueryBuilderWithClosureReturningNonQueryBuilder()
{
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => function () {
return new \stdClass();
},
));
$field->bind('2');
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
*/
public function testChoicesMustBeManaged()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
// no persist here!
$field = new EntityChoiceField('name', array(
'multiple' => false,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'choices' => array($entity1, $entity2),
'property' => 'name',
));
}
public function testSetDataSingle_null()
{
$field = new EntityChoiceField('name', array(
'multiple' => false,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
));
$field->setData(null);
$this->assertEquals(null, $field->getData());
$this->assertEquals('', $field->getDisplayedData());
}
public function testSetDataMultiple_null()
{
$field = new EntityChoiceField('name', array(
'multiple' => true,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
));
$field->setData(null);
$this->assertEquals(null, $field->getData());
$this->assertEquals(array(), $field->getDisplayedData());
}
public function testBindSingle_null()
{
$field = new EntityChoiceField('name', array(
'multiple' => false,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
));
$field->bind(null);
$this->assertEquals(null, $field->getData());
$this->assertEquals('', $field->getDisplayedData());
}
public function testBindMultiple_null()
{
$field = new EntityChoiceField('name', array(
'multiple' => true,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
));
$field->bind(null);
$this->assertEquals(new ArrayCollection(), $field->getData());
$this->assertEquals(array(), $field->getDisplayedData());
}
public function testBindSingleNonExpanded_singleIdentifier()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$this->persist(array($entity1, $entity2));
$field = new EntityChoiceField('name', array(
'multiple' => false,
'expanded' => false,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
));
$field->bind('2');
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($entity2, $field->getData());
$this->assertEquals(2, $field->getDisplayedData());
}
public function testBindSingleNonExpanded_compositeIdentifier()
{
$entity1 = new CompositeIdentEntity(10, 20, 'Foo');
$entity2 = new CompositeIdentEntity(30, 40, 'Bar');
$this->persist(array($entity1, $entity2));
$field = new EntityChoiceField('name', array(
'multiple' => false,
'expanded' => false,
'em' => $this->em,
'class' => self::COMPOSITE_IDENT_CLASS,
'property' => 'name',
));
// the collection key is used here
$field->bind('1');
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($entity2, $field->getData());
$this->assertEquals(1, $field->getDisplayedData());
}
public function testBindMultipleNonExpanded_singleIdentifier()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'multiple' => true,
'expanded' => false,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
));
$field->bind(array('1', '3'));
$expected = new ArrayCollection(array($entity1, $entity3));
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($expected, $field->getData());
$this->assertEquals(array(1, 3), $field->getDisplayedData());
}
public function testBindMultipleNonExpanded_singleIdentifier_existingData()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'multiple' => true,
'expanded' => false,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
));
$existing = new ArrayCollection(array($entity2));
$field->setData($existing);
$field->bind(array('1', '3'));
// entry with index 0 was removed
$expected = new ArrayCollection(array(1 => $entity1, 2 => $entity3));
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($expected, $field->getData());
// same object still, useful if it is a PersistentCollection
$this->assertSame($existing, $field->getData());
$this->assertEquals(array(1, 3), $field->getDisplayedData());
}
public function testBindMultipleNonExpanded_compositeIdentifier()
{
$entity1 = new CompositeIdentEntity(10, 20, 'Foo');
$entity2 = new CompositeIdentEntity(30, 40, 'Bar');
$entity3 = new CompositeIdentEntity(50, 60, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'multiple' => true,
'expanded' => false,
'em' => $this->em,
'class' => self::COMPOSITE_IDENT_CLASS,
'property' => 'name',
));
// because of the composite key collection keys are used
$field->bind(array('0', '2'));
$expected = new ArrayCollection(array($entity1, $entity3));
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($expected, $field->getData());
$this->assertEquals(array(0, 2), $field->getDisplayedData());
}
public function testBindMultipleNonExpanded_compositeIdentifier_existingData()
{
$entity1 = new CompositeIdentEntity(10, 20, 'Foo');
$entity2 = new CompositeIdentEntity(30, 40, 'Bar');
$entity3 = new CompositeIdentEntity(50, 60, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'multiple' => true,
'expanded' => false,
'em' => $this->em,
'class' => self::COMPOSITE_IDENT_CLASS,
'property' => 'name',
));
$existing = new ArrayCollection(array($entity2));
$field->setData($existing);
$field->bind(array('0', '2'));
// entry with index 0 was removed
$expected = new ArrayCollection(array(1 => $entity1, 2 => $entity3));
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($expected, $field->getData());
// same object still, useful if it is a PersistentCollection
$this->assertSame($existing, $field->getData());
$this->assertEquals(array(0, 2), $field->getDisplayedData());
}
public function testBindSingleExpanded()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$this->persist(array($entity1, $entity2));
$field = new EntityChoiceField('name', array(
'multiple' => false,
'expanded' => true,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
));
$field->bind('2');
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($entity2, $field->getData());
$this->assertSame(false, $field['1']->getData());
$this->assertSame(true, $field['2']->getData());
$this->assertSame('', $field['1']->getDisplayedData());
$this->assertSame('1', $field['2']->getDisplayedData());
$this->assertSame(array('1' => '', '2' => '1'), $field->getDisplayedData());
}
public function testBindMultipleExpanded()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Bar');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'multiple' => true,
'expanded' => true,
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
));
$field->bind(array('1' => '1', '3' => '3'));
$expected = new ArrayCollection(array($entity1, $entity3));
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($expected, $field->getData());
$this->assertSame(true, $field['1']->getData());
$this->assertSame(false, $field['2']->getData());
$this->assertSame(true, $field['3']->getData());
$this->assertSame('1', $field['1']->getDisplayedData());
$this->assertSame('', $field['2']->getDisplayedData());
$this->assertSame('1', $field['3']->getDisplayedData());
$this->assertSame(array('1' => '1', '2' => '', '3' => '1'), $field->getDisplayedData());
}
public function testOverrideChoices()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
// not all persisted entities should be displayed
'choices' => array($entity1, $entity2),
'property' => 'name',
));
$field->bind('2');
$this->assertEquals(array(1 => 'Foo', 2 => 'Bar'), $field->getOtherChoices());
$this->assertTrue($field->isTransformationSuccessful());
$this->assertEquals($entity2, $field->getData());
$this->assertEquals(2, $field->getDisplayedData());
}
public function testDisallowChoicesThatAreNotIncluded_choices_singleIdentifier()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'choices' => array($entity1, $entity2),
'property' => 'name',
));
$field->bind('3');
$this->assertFalse($field->isTransformationSuccessful());
$this->assertNull($field->getData());
}
public function testDisallowChoicesThatAreNotIncluded_choices_compositeIdentifier()
{
$entity1 = new CompositeIdentEntity(10, 20, 'Foo');
$entity2 = new CompositeIdentEntity(30, 40, 'Bar');
$entity3 = new CompositeIdentEntity(50, 60, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::COMPOSITE_IDENT_CLASS,
'choices' => array($entity1, $entity2),
'property' => 'name',
));
$field->bind('2');
$this->assertFalse($field->isTransformationSuccessful());
$this->assertNull($field->getData());
}
public function testDisallowChoicesThatAreNotIncluded_queryBuilder_singleIdentifier()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS);
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => $repository->createQueryBuilder('e')
->where('e.id IN (1, 2)'),
'property' => 'name',
));
$field->bind('3');
$this->assertFalse($field->isTransformationSuccessful());
$this->assertNull($field->getData());
}
public function testDisallowChoicesThatAreNotIncluded_queryBuilderAsClosure_singleIdentifier()
{
$entity1 = new SingleIdentEntity(1, 'Foo');
$entity2 = new SingleIdentEntity(2, 'Bar');
$entity3 = new SingleIdentEntity(3, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => function ($repository) {
return $repository->createQueryBuilder('e')
->where('e.id IN (1, 2)');
},
'property' => 'name',
));
$field->bind('3');
$this->assertFalse($field->isTransformationSuccessful());
$this->assertNull($field->getData());
}
public function testDisallowChoicesThatAreNotIncluded_queryBuilderAsClosure_compositeIdentifier()
{
$entity1 = new CompositeIdentEntity(10, 20, 'Foo');
$entity2 = new CompositeIdentEntity(30, 40, 'Bar');
$entity3 = new CompositeIdentEntity(50, 60, 'Baz');
$this->persist(array($entity1, $entity2, $entity3));
$field = new EntityChoiceField('name', array(
'em' => $this->em,
'class' => self::COMPOSITE_IDENT_CLASS,
'query_builder' => function ($repository) {
return $repository->createQueryBuilder('e')
->where('e.id1 IN (10, 50)');
},
'property' => 'name',
));
$field->bind('2');
$this->assertFalse($field->isTransformationSuccessful());
$this->assertNull($field->getData());
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Symfony\Tests\Component\Form\Extension\Doctrine\Fixtures;
/** @Entity */
class CompositeIdentEntity
{
/** @Id @Column(type="integer") */
protected $id1;
/** @Id @Column(type="integer") */
protected $id2;
/** @Column(type="string") */
public $name;
public function __construct($id1, $id2, $name) {
$this->id1 = $id1;
$this->id2 = $id2;
$this->name = $name;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Tests\Component\Form\Extension\Doctrine\Fixtures;
/** @Entity */
class SingleIdentEntity
{
/** @Id @Column(type="integer") */
protected $id;
/** @Column(type="string") */
public $name;
public function __construct($id, $name) {
$this->id = $id;
$this->name = $name;
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Form\Extension\Doctrine;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class TestCase extends \PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!class_exists('Doctrine\\Common\\Version')) {
$this->markTestSkipped('Doctrine is not available.');
}
}
/**
* @return EntityManager
*/
protected function createTestEntityManager($paths = array())
{
$config = new \Doctrine\ORM\Configuration();
$config->setAutoGenerateProxyClasses(true);
$config->setProxyDir(\sys_get_temp_dir());
$config->setProxyNamespace('SymfonyTests\Doctrine');
$config->setMetadataDriverImpl($config->newDefaultAnnotationDriver($paths));
$config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache());
$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache());
$params = array(
'driver' => 'pdo_sqlite',
'memory' => true,
);
return EntityManager::create($params, $config);
}
}