Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Form] Refactored choice lists to support dynamic label, value, index and attribute generation #14050

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

Merged
merged 9 commits into from
Apr 1, 2015
Merged
171 changes: 171 additions & 0 deletions src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Common\Persistence\ObjectManager;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;

/**
* Loads choices using a Doctrine object manager.
*
* @author Bernhard Schussek <[email protected]>
*/
class DoctrineChoiceLoader implements ChoiceLoaderInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $factory;

/**
* @var ObjectManager
*/
private $manager;

/**
* @var string
*/
private $class;

/**
* @var IdReader
*/
private $idReader;

/**
* @var null|EntityLoaderInterface
*/
private $objectLoader;

/**
* @var ChoiceListInterface
*/
private $choiceList;

/**
* Creates a new choice loader.
*
* Optionally, an implementation of {@link EntityLoaderInterface} can be
* passed which optimizes the object loading for one of the Doctrine
* mapper implementations.
*
* @param ChoiceListFactoryInterface $factory The factory for creating
* the loaded choice list
* @param ObjectManager $manager The object manager
* @param string $class The class name of the
* loaded objects
* @param IdReader $idReader The reader for the object
* IDs.
* @param null|EntityLoaderInterface $objectLoader The objects loader
*/
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader, EntityLoaderInterface $objectLoader = null)
{
$this->factory = $factory;
$this->manager = $manager;
$this->class = $manager->getClassMetadata($class)->getName();
$this->idReader = $idReader;
$this->objectLoader = $objectLoader;
}

/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if ($this->choiceList) {
return $this->choiceList;
}

$objects = $this->objectLoader
? $this->objectLoader->getEntities()
: $this->manager->getRepository($this->class)->findAll();

$this->choiceList = $this->factory->createListFromChoices($objects, $value);

return $this->choiceList;
}

/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Performance optimization
if (empty($choices)) {
return array();
}

// Optimize performance for single-field identifiers. We already
// know that the IDs are used as values

// Attention: This optimization does not check choices for existence
if (!$this->choiceList && $this->idReader->isSingleId()) {
$values = array();

// Maintain order and indices of the given objects
foreach ($choices as $i => $object) {
if ($object instanceof $this->class) {
// Make sure to convert to the right format
$values[$i] = (string) $this->idReader->getIdValue($object);
}
}

return $values;
}

return $this->loadChoiceList($value)->getValuesForChoices($choices);
}

/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

inheritdoc? same for loadValuesForChoices

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

{
// Performance optimization
// Also prevents the generation of "WHERE id IN ()" queries through the
// object loader. At least with MySQL and on the development machine
// this was tested on, no exception was thrown for such invalid
// statements, consequently no test fails when this code is removed.
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
if (empty($values)) {
return array();
}

// Optimize performance in case we have an object loader and
// a single-field identifier
if (!$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) {
$unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values);
$objectsById = array();
$objects = array();

// Maintain order and indices from the given $values
// An alternative approach to the following loop is to add the
// "INDEX BY" clause to the Doctrine query in the loader,
// but I'm not sure whether that's doable in a generic fashion.
foreach ($unorderedObjects as $object) {
$objectsById[$this->idReader->getIdValue($object)] = $object;
}

foreach ($values as $i => $id) {
if (isset($objectsById[$id])) {
$objects[$i] = $objectsById[$id];
}
}

return $objects;
}

return $this->loadChoiceList($value)->getChoicesForValues($values);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@

namespace Symfony\Bridge\Doctrine\Form\ChoiceList;

use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;

/**
* A choice list presenting a list of Doctrine entities as choices.
*
* @author Bernhard Schussek <[email protected]>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link DoctrineChoiceLoader} instead.
*/
class EntityChoiceList extends ObjectChoiceList
{
Expand Down Expand Up @@ -126,6 +129,8 @@ public function __construct(ObjectManager $manager, $class, $labelPath = null, E
}

parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor);

trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't we move this near the namespace declaration ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what do you mean?

Copy link
Member

Choose a reason for hiding this comment

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

trigger_error('The '.__NAMESPACE__.'\LegacyValidator class is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED);

this way the trigger happens the first time the class is autoloaded

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #14201.

}

/**
Expand Down
125 changes: 125 additions & 0 deletions src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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;
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 moving the class out of the Form namespace? The UniqueEntityConstraintValidator and the EntityUserProvider use some similar logic to retrieve id values for entities. Maybe we can reuse this code there.

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 think that doesn't really make sense. ChoiceList is very form specific (values are always strings, names must follow the naming conventions of forms, ...). Making ChoiceList more generic would just make maintenance harder, and I'd like to avoid that - maintenance of the Form component is hard enough as it is.


use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\RuntimeException;

/**
* A utility for reading object IDs.
*
* @since 1.0
* @author Bernhard Schussek <[email protected]>
*
* @internal This class is meant for internal use only.
*/
class IdReader
{
/**
* @var ObjectManager
*/
private $om;

/**
* @var ClassMetadata
*/
private $classMetadata;

/**
* @var bool
*/
private $singleId;

/**
* @var bool
*/
private $intId;

/**
* @var string
*/
private $idField;

public function __construct(ObjectManager $om, ClassMetadata $classMetadata)
{
$ids = $classMetadata->getIdentifierFieldNames();
$idType = $classMetadata->getTypeOfField(current($ids));

$this->om = $om;
$this->classMetadata = $classMetadata;
$this->singleId = 1 === count($ids);
$this->intId = $this->singleId && 1 === count($ids) && in_array($idType, array('integer', 'smallint', 'bigint'));
Copy link
Member

Choose a reason for hiding this comment

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

1 === count($ids) is useless here. It is already checked by $this->singleId

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #14202.

$this->idField = current($ids);
}

/**
* Returns whether the class has a single-column ID.
*
* @return bool Returns `true` if the class has a single-column ID and
* `false` otherwise.
*/
public function isSingleId()
{
return $this->singleId;
}

/**
* Returns whether the class has a single-column integer ID.
*
* @return bool Returns `true` if the class has a single-column integer ID
* and `false` otherwise.
*/
public function isIntId()
{
return $this->intId;
}

/**
* Returns the ID value for an object.
*
* This method assumes that the object has a single-column ID.
*
* @param object $object The object.
*
* @return mixed The ID value.
*/
public function getIdValue($object)
{
if (!$object) {
return;
}

if (!$this->om->contains($object)) {
throw new RuntimeException(
'Entities passed to the choice field must be managed. Maybe '.
'persist them in the entity manager?'
);
}

$this->om->initializeObject($object);

return current($this->classMetadata->getIdentifierValues($object));
}

/**
* Returns the name of the ID field.
*
* This method assumes that the object has a single-column ID.
*
* @return string The name of the ID field.
*/
public function getIdField()
{
return $this->idField;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
use Doctrine\ORM\EntityManager;

/**
* Getting Entities through the ORM QueryBuilder.
* Loads entities using a {@link QueryBuilder} instance.
*
* @author Benjamin Eberlei <[email protected]>
* @author Bernhard Schussek <[email protected]>
*/
class ORMQueryBuilderLoader implements EntityLoaderInterface
{
Expand All @@ -34,9 +37,14 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
/**
* Construct an ORM Query Builder Loader.
*
* @param QueryBuilder|\Closure $queryBuilder
* @param EntityManager $manager
* @param string $class
* @param QueryBuilder|\Closure $queryBuilder The query builder or a closure
* for creating the query builder.
* Passing a closure is
* deprecated and will not be
* supported anymore as of
* Symfony 3.0.
* @param EntityManager $manager Deprecated.
* @param string $class Deprecated.
*
* @throws UnexpectedTypeException
*/
Expand All @@ -49,10 +57,15 @@ public function __construct($queryBuilder, $manager = null, $class = null)
}

if ($queryBuilder instanceof \Closure) {
trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
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 a very hard deprecation. All my entity fields in my project are relying on this feature for instance. Providing an alternative for the upgrade is absolutely necessary. The message does not explain what to do instead

Copy link
Contributor

Choose a reason for hiding this comment

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

This is only for the ORMQueryBuilderLoader which you probably do not use standalone. If you pass a closure to the correspoding option, it still works.

Copy link
Member

Choose a reason for hiding this comment

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

OK, nevermind. the form itself still accepts closures. the resolution has only been moved to a different place


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

trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);

$queryBuilder = $queryBuilder($manager->getRepository($class));

if (!$queryBuilder instanceof QueryBuilder) {
Expand Down
Loading