8000 [WIP] [DoctrineBridge] Refactor EntityChoiceList by beberlei · Pull Request #2728 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[WIP] [DoctrineBridge] Refactor EntityChoiceList #2728

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 27 additions & 71 deletions src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php
8000
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\NoResultException;

Expand Down Expand Up @@ -52,7 +52,7 @@ class EntityChoiceList extends ArrayChoiceList
*
* @var Doctrine\ORM\QueryBuilder
*/
private $queryBuilder;
private $entityLoader;

/**
* The fields of which the identifier of the underlying class consists
Expand All @@ -63,22 +63,18 @@ class EntityChoiceList extends ArrayChoiceList
*/
private $identifier = array();

/**
* A cache for \ReflectionProperty instances for the underlying class
*
* This property should only be accessed through getReflProperty().
*
* @var array
*/
private $reflProperties = array();

/**
* A cache for the UnitOfWork instance of Doctrine
*
* @var Doctrine\ORM\UnitOfWork
*/
private $unitOfWork;

/**
* Property path to access the key value of this choice-list.
*
* @var PropertyPath
*/
private $propertyPath;

/**
Expand All @@ -88,33 +84,22 @@ class EntityChoiceList extends ArrayChoiceList
*/
private $groupBy;

public function __construct(EntityManager $em, $class, $property = null, $queryBuilder = null, $choices = array(), $groupBy = null)
public function __construct(ObjectManager $manager, $class, $property = null, EntityLoaderInterface $entityLoader = null, $choices = array(), $groupBy = null)
{
// If a query builder was passed, it must be a closure or QueryBuilder
// instance
if (!(null === $queryBuilder || $queryBuilder instanceof QueryBuilder || $queryBuilder instanceof \Closure)) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
}

if ($queryBuilder instanceof \Closure) {
$queryBuilder = $queryBuilder($em->getRepository($class));

if (!$queryBuilder instanceof QueryBuilder) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
}
}

$this->em = $em;
$this->em = $manager;
$this->class = $class;
$this->queryBuilder = $queryBuilder;
$this->unitOfWork = $em->getUnitOfWork();
$this->identifier = $em->getClassMetadata($class)->getIdentifierFieldNames();
$this->entityLoader = $entityLoader;
$this->unitOfWork = $manager->getUnitOfWork();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not part of the interface. Nothing enforces have a UnitOfWork when implementing the interface

$this->identifier = $manager->getClassMetadata($class)->getIdentifierFieldNames();
$this->groupBy = $groupBy;

// The property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
} else if (!method_exists($this->class, '__toString')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be elseif

// Otherwise expect a __toString() method in the entity
throw new FormException('Entities passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option).');
}

parent::__construct($choices);
Expand All @@ -137,8 +122,8 @@ protected function load()

if ($this->choices) {
$entities = $this->choices;
} else if ($qb = $this->queryBuilder) {
$entities = $qb->getQuery()->execute();
} else if ($entityLoader = $this->entityLoader) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, why using a local variable here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i copied bernhards style, no other reason-

$entities = $entityLoader->getEntities();
} else {
$entities = $this->em->getRepository($this->class)->findAll();
}
Expand All @@ -158,11 +143,11 @@ protected function load()
private function groupEntities($entities, $groupBy)
{
$grouped = array();
$path = new PropertyPath($groupBy);

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
Expand Down Expand Up @@ -258,7 +243,7 @@ public function getEntities()
}

/**
* Returns the entity for the given key
* Returns the entities for the given keys
*
* If the underlying entities have composite identifiers, the choices
* are initialized. The key is expected to be the index in the choices
Expand All @@ -267,54 +252,25 @@ public function getEntities()
* 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
* @param array $keys The choice key (for entities with composite
* identifiers) or entity ID (for entities with single
* identifiers)
* @return object The matching entity
* @return object[] The matching entity
*/
public function getEntity($key)
public function getEntitiesByKeys(array $keys)
{
if (!$this->loaded) {
$this->load();
}
$found = array();

try {
if (count($this->identifier) > 1) {
// $key is a collection index
$entities = $this->getEntities();

return isset($entities[$key]) ? $entities[$key] : null;
} else if ($this->entities) {
return isset($this->entities[$key]) ? $this->entities[$key] : null;
} else if ($qb = $this->queryBuilder) {
// should we clone the builder?
$alias = $qb->getRootAlias();
$where = $qb->expr()->eq($alias.'.'.current($this->identifier), $key);

return $qb->andWhere($where)->getQuery()->getSingleResult();
foreach ($keys as $key) {
if (isset($this->entities[$key])) {
$found[] = $this->entities[$key];
}

return $this->em->find($this->class, $key);
} catch (NoResultException $e) {
return null;
}
}

/**
* Returns the \ReflectionProperty instance for a property of the
* underlying class
*
* @param string $property The name of the property
* @return \ReflectionProperty The reflection instance
*/
private function getReflProperty($property)
{
if (!isset($this->reflProperties[$property])) {
$this->reflProperties[$property] = new \ReflectionProperty($this->class, $property);
$this->reflProperties[$property]->setAccessible(true);
}

return $this->reflProperties[$property];
return $found;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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\Bridge\Doctrine\Form\ChoiceList;

/**
* Custom loader for entities in the choice list.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
interface EntityLoaderInterface
{
/**
* Return an array of entities that are valid choices in the corresponding choice list.
*
* @return array
*/
function getEntities();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?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\Bridge\Doctrine\Form\ChoiceList;

use Doctrine\DBAL\Connection;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Doctrine\ORM\QueryBuilder;

/**
* Getting Entities through the ORM QueryBuilder
*/
class ORMQueryBuilderLoader implements EntityLoaderInterface
{
/**
* Contains the query builder that builds the query for fetching the
* entities
*
* This property should only be accessed through queryBuilder.
*
* @var Doctrine\ORM\QueryBuilder
*/
private $queryBuilder;

/**
* Construct an ORM Query Builder Loader
*
* @param QueryBuilder $queryBuilder
* @param EntityManager $manager
* @param string $class
*/
public function __construct($queryBuilder, $manager = null, $class = null)
{
// If a query builder was passed, it must be a closure or QueryBuilder
// instance
if (!($queryBuilder instanceof QueryBuilder || $queryBuilder instanceof \Closure)) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
}

if ($queryBuilder instanceof \Closure) {
$queryBuilder = $queryBuilder($manager->getRepository($class));

if (!$queryBuilder instanceof QueryBuilder) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
}
}

$this->queryBuilder = $queryBuilder;
}

/**
* {@inheritDoc}
*/
public function getEntities()
{
return $this->queryBuilder->getQuery()->execute();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about the case where $this->queryBuilder is null (which is valid in the constructor) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, null is not valid anymore.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function transform($collection)

foreach ($collection as $entity) {
// identify choices by their collection key
$key = array_search($entity, $availableEntities);
$key = array_search($entity, $availableEntities, true);
$array[] = $key;
}
} else {
Expand Down Expand Up @@ -84,19 +84,13 @@ public function reverseTransform($keys)
throw new UnexpectedTypeException($keys, 'array');
}

$notFound = array();

// optimize this into a SELECT WHERE IN query
foreach ($keys as $key) {
if ($entity = $this->choiceList->getEntity($key)) {
$collection->add($entity);
} else {
$notFound[] = $key;
}
$entities = $this->choiceList->getEntitiesByKeys($keys);
if (count($keys) !== count($entities)) {
throw new TransformationFailedException('Not all entities matching the keys were found.');
}

if (count($notFound) > 0) {
throw new TransformationFailedException(sprintf('The entities with keys "%s" could not be found', implode('", "', $notFound)));
foreach ($entities as $entity) {
$collection->add($entity);
}

return $collection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ public function reverseTransform($key)
throw new UnexpectedTypeException($key, 'numeric');
}

if (!($entity = $this->choiceList->getEntity($key))) {
if (!($entities = $this->choiceList->getEntitiesByKeys(array($key)))) {
throw new TransformationFailedException(sprintf('The entity with key "%s" could not be found', $key));
}

return $entity;
return $entities[0];
}
}
30 changes: 28 additions & 2 deletions src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php
F42D
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Form\FormBuilder;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\EntitiesToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\DataTransformer\EntityToIdTransformer;
use Symfony\Component\Form\AbstractType;

class EntityType extends AbstractType
{
/**
* @var ManagerRegistry
*/
protected $registry;

public function __construct(ManagerRegistry $registry)
Expand All @@ -47,18 +51,24 @@ public function getDefaultOptions(array $options)
'class' => null,
'property' => null,
'query_builder' => null,
'loader' => null,
'choices' => array(),
'group_by' => null,
);

$options = array_replace($defaultOptions, $options);

if (!isset($options['choice_list'])) {
$manager = $this->registry->getManager($options['em']);
if (isset($options['query_builder'])) {
$options['loader'] = $this->getLoader($manager, $options);
}

$defaultOptions['choice_list'] = new EntityChoiceList(
$this->registry->getManager($options['em']),
$manager,
$options['class'],
$options['property'],
$options['query_builder'],
$options['loader'],
$options['choices'],
$options['group_by']
);
Expand All @@ -67,6 +77,22 @@ public function getDefaultOptions(array $options)
return $defaultOptions;
}

/**
* Return the default loader object.
*
* @param ObjectManager
* @param array $options
* @return ORMQueryBuilderLoader
*/
protected function getLoader($manager, $options)
{
return new ORMQueryBuilderLoader(
$options['query_builder'],
$manager,
$options['class']
);
}

public function getParent(array $options)
{
return 'choice';
Expand Down
Loading
0