8000 [Form] Refactored choice lists to support dynamic label, value, index and attribute generation by webmozart · Pull Request #14050 · symfony/symfony · GitHub
[go: up one dir, main page]

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
Prev Previous commit
Next Next commit
[Form] Fixed PR comments
  • Loading branch information
webmozart committed Apr 1, 2015
commit d6179c830be7f2245ad56b6a800a33275a802689
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,12 @@ public function loadChoiceList($value = null)
}

/**
* Loads the values corresponding to the given objects.
*
* The values are returned with the same keys and in the same order as the
* corresponding objects in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the object as first and the array key as the second
* argument.
*
* @param array $objects An array of objects. Non-existing objects in
* this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return string[] An array of choice values
* {@inheritdoc}
*/
public function loadValuesForChoices(array $objects, $value = null)
public function loadValuesForChoices(array $choices, $value = null)
{
// Performance optimization
if (empty($objects)) {
if (empty($choices)) {
return array();
}

Expand All @@ -127,7 +114,7 @@ public function loadValuesForChoices(array $objects, $value = null)
$values = array();

// Maintain order and indices of the given objects
foreach ($objects as $i => $object) {
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);
Expand All @@ -137,24 +124,11 @@ public function loadValuesForChoices(array $objects, $value = null)
return $values;
}

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

/**
* Loads the objects corresponding to the given values.
*
* The objects are returned with the same keys and in the same order as the
* corresponding values in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the object as first and the array key as the second
* argument.
*
* @param string[] $values An array of choice values. Non-existing
* values in this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return array An array of objects
* {@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

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public function __construct(ObjectManager $manager, $class, $labelPath = null, E
628C }

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
Original file line number Diff line number Diff line change
Expand Up @@ -37,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 @@ -51,13 +56,16 @@ public function __construct($queryBuilder, $manager = null, $class = null)
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
}

// This block is not executed anymore since Symfony 2.7. The query
// builder closure is already invoked in DoctrineType
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
65 changes: 43 additions & 22 deletions src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,10 @@ public function configureOptions(OptionsResolver $resolver)
$choiceLoaders = &$this->choiceLoaders;
$type = $this;

$idReader = function (Options $options) use (&$idReaders) {
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
));

// The ID reader is a utility that is needed to read the object IDs
// when generating the field values. The callback generating the
// field values has no access to the object manager or the class
// of the field, so we store that information in the reader.
// The reader is cached so that two choice lists for the same class
// (and hence with the same reader) can successfully be cached.
if (!isset($idReaders[$hash])) {
$classMetadata = $options['em']->getClassMetadata($options['class']);
$idReaders[$hash] = new IdReader($options['em'], $classMetadata);
}

return $idReaders[$hash];
};

$choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) {
// This closure and the "query_builder" options should be pushed to
// EntityType in Symfony 3.0 as they are specific to the ORM
Copy link
Member

Choose a reason for hiding this comment

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

Are they ? the ODM type also allows to pass a query builder (your new logic to build the cache is specific to the ORM indeed, but this is a BC break)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, because the ODM query builder is of a different class. Both types should support the "query_builder" option, but both with their own logic.

Copy link
Member

Choose a reason for hiding this comment

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

but the DoctrineType is the class holding the logic being shared by all Doctrine O*M projects. If it is specific to the ORM, it must go in the EntityType

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, in fact I'd like to move "query_builder" to EntityType - but that would break BC.


// Unless the choices are given explicitly, load them on demand
if (null === $options['choices']) {
// We consider two query builders with an equal SQL string and
Expand Down Expand Up @@ -243,6 +226,13 @@ public function configureOptions(OptionsResolver $resolver)
return $em;
};

// deprecation note
$propertyNormalizer = function (Options $options, $propertyName) {
trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED);
Copy link
Contributor

Choose a reason for hiding this comment

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

This normalizer is called for BC by default in the choice_label option. So this will trigger the deprecation notice even if not used. I guess we have to test if null !== $propertyName and only then trigger the notice in order to only warn when the option is actually used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same for the other option deprecations.

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


return $propertyName;
};

// Invoke the query builder closure so that we can cache choice lists
// for equal query builders
$queryBuilderNormalizer = function (Options $options, $queryBuilder) {
Expand All @@ -257,6 +247,35 @@ public function configureOptions(OptionsResolver $resolver)
return $queryBuilder;
};

// deprecation note
$loaderNormalizer = function (Options $options, $loader) {
trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED);

return $loader;
};

// Set the "id_reader" option via the normalizer. This option is not
// supposed to be set by the user.
$idReaderNormalizer = function (Options $options) use (&$idReaders) {
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
));

// The ID reader is a utility that is needed to read the object IDs
// when generating the field values. The callback generating the
// field values has no access to the object manager or the class
// of the field, so we store that information in the reader.
// The reader is cached so that two choice lists for the same class
// (and hence with the same reader) can successfully be cached.
if (!isset($idReaders[$hash])) {
$classMetadata = $options['em']->getClassMetadata($options['class']);
$idReaders[$hash] = new IdReader($options['em'], $classMetadata);
}

return $idReaders[$hash];
};

$resolver->setDefaults(array(
'em' => null,
'property' => null, // deprecated, use "choice_label"
Copy link
Contributor

Choose a reason for hiding this comment

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

needs trigger via normalizer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

Expand All @@ -268,17 +287,19 @@ public function configureOptions(OptionsResolver $resolver)
'choice_label' => $choiceLabel,
'choice_name' => $choiceName,
'choice_value' => $choiceValue,
'id_reader' => $idReader,
'id_reader' => null, // internal
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it not make more sense to implement this as a method just like getLoader on the type? As this is not an actual option it feels misused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is that we need to store the IdReader object (that depends on the "em" and "class" options) with the Form instance somehow. We only have two mechanisms for that:

  • options
  • attributes

Attributes don't work, since we need to access the ID reader already before the Form instance is built (from the other option closures). So this to me seems like the only alternative.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes my first suggestion wasn't ideal. But I don't see why it should not be possible to use that idReader closure (which is just about reusing code) without exposing it as option. Maybe I'm missing something, but how about:

$idReader = function ($em, $class) use (&$idReaders) {
    $hash = CachingFactoryDecorator::generateHash(array(
        $em,
        $class,
    ));
    // ...
}

$choiceValue = function (Options $options) use ($idReader) {
    /** @var IdReader $idReader */
    $idReader = $idReader($options['em'], $options['class']);
    // or shorter as $idReader = $idReader($options);
    // ...
}

This way there is no exposed option and it should still do the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then the ID reader would be created three times, as it is used in three closures. You could of course implement some kind of caching, but that makes the code of each closure even more complex.

The "id_reader" option is marked as internal, so it should not be used. I think that should be sufficient.

));

$resolver->setRequired(array('class'));

$resolver->setNormalizer('em', $emNormalizer);
$resolver->setNormalizer('property', $propertyNormalizer);
$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
$resolver->setNormalizer('loader', $loaderNormalizer);
$resolver->setNormalizer('id_reader', $idReaderNormalizer);

$resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'));
$resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'));
$resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder'));
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class EntityType extends DoctrineType
/**
* Return the default loader object.
*
* @param ObjectManager $manager
* @param QueryBuilder|\Closure $queryBuilder
* @param string $class
* @param ObjectManager $manager
* @param QueryBuilder $queryBuilder
* @param string $class
*
* @return ORMQueryBuilderLoader
*/
Expand Down
8 changes: 4 additions & 4 deletions src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ class ArrayChoiceList implements ChoiceListInterface
*
* The given choice array must have the same array keys as the value array.
*
* @param array $choices The selectable choices
* @param callable $value The callable for creating the value for a
* choice. If `null` is passed, incrementing
* integers are used as values
* @param array $choices The selectable choices
* @param callable|null $value The callable for creating the value for a
* choice. If `null` is passed, incrementing
* integers are used as values
*/
public function __construct(array $choices, $value = null)
{
Expand Down
4 changes: 3 additions & 1 deletion src/Symfony/Component/Form/ChoiceList/ArrayKeyChoiceList.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be removed
* in Symfony 3.0.
* in Symfony 3.0. Use {@link ArrayChoiceList} instead.
*/
class ArrayKeyChoiceList extends ArrayChoiceList
{
Expand Down Expand Up @@ -113,6 +113,8 @@ public function __construct(array $choices, $value = null)
}

parent::__construct($choices, $value);

trigger_error('The '.__CLASS__.' class was added for backwards compatibility in version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,6 @@ public function getDecoratedFactory()
*/
public function createListFromChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}

if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
Expand Down Expand Up @@ -124,10 +120,6 @@ public function createListFromChoices($choices, $value = null)
*/
public function createListFromFlippedChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}

if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ public static function flattenFlipped(array $array, &$output)
*/
public function createListFromChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}

if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
Expand All @@ -109,10 +105,6 @@ public function createListFromChoices($choices, $value = null)
*/
public function createListFromFlippedChoices($choices, $value = null)
{
if (!is_array($choices) && !$choices instanceof \Traversable) {
throw new UnexpectedTypeException($choices, 'array or \Traversable');
}

if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
Expand Down Expand Up @@ -140,10 +132,6 @@ public function createListFromFlippedChoices($choices, $value = null)
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
if (null !== $value && !is_callable($value)) {
throw new UnexpectedTypeException($value, 'null or callable');
}

return new LazyChoiceList($loader, $value);
}

Expand All @@ -152,26 +140,6 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
if (null !== $preferredChoices && !is_array($preferredChoices) && !is_callable($preferredChoices)) {
throw new UnexpectedTypeException($preferredChoices, 'null, array or callable');
}

if (null !== $label && !is_callable($label)) {
throw new UnexpectedTypeException($label, 'null or callable');
}

if (null !== $index && !is_callable($index)) {
throw new UnexpectedTypeException($index, 'null or callable');
}

if (null !== $groupBy && !is_array($groupBy) && !$groupBy instanceof \Traversable && !is_callable($groupBy)) {
throw new UnexpectedTypeException($groupBy, 'null, array, \Traversable or callable');
}

if (null !== $attr && !is_array($attr) && !is_callable($attr)) {
throw new UnexpectedTypeException($attr, 'null, array or callable');
}

// Backwards compatibility
if ($list instanceof LegacyChoiceListInterface && null === $preferredChoices
&& null === $label && null === $index && null === $groupBy && null === $attr) {
Expand Down Expand Up @@ -247,7 +215,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
);
}

// Remove any empty group views that may have been created by
// Remove any empty group view that may have been created by
// addChoiceViewGroupedBy()
foreach ($preferredViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,12 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul
/**
* {@inheritdoc}
*
* @param ChoiceListInterface $list The choice list
* @param null|array|callable|PropertyPath $preferredChoices The preferred choices
* @param null|callable|PropertyPath $label The callable or path
* generating the choice labels
* @param null|callable|PropertyPath $index The callable or path
* generating the view indices
* @param null|array|\Traversable|callable|PropertyPath $groupBy The callable or path
* generating the group names
* @param null|array|callable|PropertyPath $attr The callable or path
* generating the HTML attributes
* @param ChoiceListInterface $list The choice list
* @param null|array|callable|string|PropertyPath $preferredChoices The preferred choices
* @param null|callable|string|PropertyPath $label The callable or path generating the choice labels
* @param null|callable|string|PropertyPath $index The callable or path generating the view indices
* @param null|array|\Traversable|callable|string|PropertyPath $groupBy The callable or path generating the group names
* @param null|array|callable|string|PropertyPath $attr The callable or path generating the HTML attributes
*
* @return ChoiceListView The choice list view
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Form/ChoiceList/LazyChoiceList.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class LazyChoiceList implements ChoiceListInterface
private $compareByValue;

/**
* @var ChoiceListInterface
* @var ChoiceListInterface|null
*/
private $loadedList;

Expand Down
Loading
0