[Form] Added support for caching choice lists based on options

This commit is contained in:
Jules Pietri 2019-04-07 22:07:24 +02:00 committed by Jules Pietri
parent fcb833f26d
commit b25973cc2e
No known key found for this signature in database
GPG Key ID: C924CC98D39AA885
21 changed files with 838 additions and 122 deletions

View File

@ -20,6 +20,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\FormBuilderInterface;
@ -40,9 +41,9 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
private $idReaders = [];
/**
* @var DoctrineChoiceLoader[]
* @var EntityLoaderInterface[]
*/
private $choiceLoaders = [];
private $entityLoaders = [];
/**
* Creates the label for a choice.
@ -115,43 +116,26 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
$choiceLoader = function (Options $options) {
// Unless the choices are given explicitly, load them on demand
if (null === $options['choices']) {
$hash = null;
$qbParts = null;
// If there is no QueryBuilder we can safely cache
$vary = [$options['em'], $options['class']];
// If there is no QueryBuilder we can safely cache DoctrineChoiceLoader,
// also if concrete Type can return important QueryBuilder parts to generate
// hash key we go for it as well
if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
$hash = CachingFactoryDecorator::generateHash([
$options['em'],
$options['class'],
$qbParts,
]);
if (isset($this->choiceLoaders[$hash])) {
return $this->choiceLoaders[$hash];
}
// hash key we go for it as well, otherwise fallback on the instance
if ($options['query_builder']) {
$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
}
if (null !== $options['query_builder']) {
$entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']);
} else {
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
$entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']);
}
$doctrineChoiceLoader = new DoctrineChoiceLoader(
return ChoiceList::loader($this, new DoctrineChoiceLoader(
$options['em'],
$options['class'],
$options['id_reader'],
$entityLoader
);
if (null !== $hash) {
$this->choiceLoaders[$hash] = $doctrineChoiceLoader;
}
return $doctrineChoiceLoader;
$this->getCachedEntityLoader(
$options['em'],
$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
$options['class'],
$vary
)
), $vary);
}
return null;
@ -162,7 +146,7 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
// field name. We can only use numeric IDs as names, as we cannot
// guarantee that a non-numeric ID contains a valid form name
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
return [__CLASS__, 'createChoiceName'];
return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);
}
// Otherwise, an incrementing integer is used as name automatically
@ -176,7 +160,7 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
$choiceValue = function (Options $options) {
// If the entity has a single-column ID, use that ID as value
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
return [$options['id_reader'], 'getIdValue'];
return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']);
}
// Otherwise, an incrementing integer is used as value automatically
@ -214,27 +198,13 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
// Set the "id_reader" option via the normalizer. This option is not
// supposed to be set by the user.
$idReaderNormalizer = function (Options $options) {
$hash = CachingFactoryDecorator::generateHash([
$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($this->idReaders[$hash])) {
$classMetadata = $options['em']->getClassMetadata($options['class']);
$this->idReaders[$hash] = new IdReader($options['em'], $classMetadata);
}
if ($this->idReaders[$hash]->isSingleId()) {
return $this->idReaders[$hash];
}
return null;
return $this->getCachedIdReader($options['em'], $options['class']);
};
$resolver->setDefaults([
@ -242,7 +212,7 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
'query_builder' => null,
'choices' => null,
'choice_loader' => $choiceLoader,
'choice_label' => [__CLASS__, 'createChoiceLabel'],
'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
'choice_name' => $choiceName,
'choice_value' => $choiceValue,
'id_reader' => null, // internal
@ -274,6 +244,27 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
public function reset()
{
$this->choiceLoaders = [];
$this->entityLoaders = [];
}
private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader
{
$hash = CachingFactoryDecorator::generateHash([$manager, $class]);
if (isset($this->idReaders[$hash])) {
return $this->idReaders[$hash];
}
$idReader = new IdReader($manager, $manager->getClassMetadata($class));
// don't cache the instance for composite ids that cannot be optimized
return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null;
}
private function getCachedEntityLoader(ObjectManager $manager, $queryBuilder, string $class, array $vary): EntityLoaderInterface
{
$hash = CachingFactoryDecorator::generateHash($vary);
return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class));
}
}

View File

@ -1205,13 +1205,13 @@ class EntityTypeTest extends BaseTypeTest
'property3' => 2,
]);
$choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader');
$choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader');
$choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader');
$choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list');
$choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list');
$choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list');
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1);
$this->assertSame($choiceLoader1, $choiceLoader2);
$this->assertSame($choiceLoader1, $choiceLoader3);
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1);
$this->assertSame($choiceList1, $choiceList2);
$this->assertSame($choiceList1, $choiceList3);
}
public function testLoaderCachingWithParameters()
@ -1265,13 +1265,13 @@ class EntityTypeTest extends BaseTypeTest
'property3' => 2,
]);
$choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader');
$choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader');
$choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader');
$choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list');
$choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list');
$choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list');
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1);
$this->assertSame($choiceLoader1, $choiceLoader2);
$this->assertSame($choiceLoader1, $choiceLoader3);
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1);
$this->assertSame($choiceList1, $choiceList2);
$this->assertSame($choiceList1, $choiceList3);
}
protected function createRegistryMock($name, $em)

View File

@ -4,6 +4,7 @@ CHANGELOG
5.1.0
-----
* Added a `ChoiceList` facade to leverage explicit choice list caching based on options
* Added an `AbstractChoiceLoader` to simplify implementations and handle global optimizations
* The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured.
* Added default `inputmode` attribute to Search, Email and Tel form types.

View File

@ -0,0 +1,135 @@
<?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\Component\Form\ChoiceList;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A set of convenient static methods to create cacheable choice list options.
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceList
{
/**
* Creates a cacheable loader from any callable providing iterable choices.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $choices A callable that must return iterable choices or grouped choices
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader
*/
public static function lazy($formType, callable $choices, $vary = null): ChoiceLoader
{
return self::loader($formType, new CallbackChoiceLoader($choices), $vary);
}
/**
* Decorates a loader to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader
*/
public static function loader($formType, ChoiceLoaderInterface $loader, $vary = null): ChoiceLoader
{
return new ChoiceLoader($formType, $loader, $vary);
}
/**
* Decorates a "choice_value" callback to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $value Any pseudo callable to create a unique string value from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function value($formType, $value, $vary = null): ChoiceValue
{
return new ChoiceValue($formType, $value, $vary);
}
/**
* Decorates a "choice_label" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function label($formType, $label, $vary = null): ChoiceLabel
{
return new ChoiceLabel($formType, $label, $vary);
}
/**
* Decorates a "choice_name" callback to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $fieldName Any pseudo callable to create a field name from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function fieldName($formType, $fieldName, $vary = null): ChoiceFieldName
{
return new ChoiceFieldName($formType, $fieldName, $vary);
}
/**
* Decorates a "choice_attr" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|array $attr Any pseudo callable or array to create html attributes from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function attr($formType, $attr, $vary = null): ChoiceAttr
{
return new ChoiceAttr($formType, $attr, $vary);
}
/**
* Decorates a "group_by" callback to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable $groupBy Any pseudo callable to return a group name from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
*/
public static function groupBy($formType, $groupBy, $vary = null): GroupBy
{
return new GroupBy($formType, $groupBy, $vary);
}
/**
* Decorates a "preferred_choices" option to make it cacheable.
*
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param callable|array $preferred Any pseudo callable or array to return a group name from a choice
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
public static function preferred($formType, $preferred, $vary = null): PreferredChoice
{
return new PreferredChoice($formType, $preferred, $vary);
}
/**
* Should not be instantiated.
*/
private function __construct()
{
}
}

View File

@ -0,0 +1,64 @@
<?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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A template decorator for static {@see ChoiceType} options.
*
* Used as fly weight for {@see CachingFactoryDecorator}.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
abstract class AbstractStaticOption
{
private static $options = [];
/** @var bool|callable|string|array|\Closure|ChoiceLoaderInterface */
private $option;
/**
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* @param mixed $option Any pseudo callable, array, string or bool to define a choice list option
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
final public function __construct($formType, $option, $vary = null)
{
if (!$formType instanceof FormTypeInterface && !$formType instanceof FormTypeExtensionInterface) {
throw new \TypeError(sprintf('Expected an instance of "%s" or "%s", but got "%s".', FormTypeInterface::class, FormTypeExtensionInterface::class, \is_object($formType) ? \get_class($formType) : \gettype($formType)));
}
$hash = CachingFactoryDecorator::generateHash([static::class, $formType, $vary]);
$this->option = self::$options[$hash] ?? self::$options[$hash] = $option;
}
/**
* @return mixed
*/
final public function getOption()
{
return $this->option;
}
final public static function reset(): void
{
self::$options = [];
}
}

View File

@ -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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_attr" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceAttr extends AbstractStaticOption
{
}

View File

@ -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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_name" callback.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceFieldName extends AbstractStaticOption
{
}

View File

@ -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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_label" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceLabel extends AbstractStaticOption
{
}

View File

@ -0,0 +1,51 @@
<?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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_loader" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface
{
/**
* {@inheritdoc}
*/
public function loadChoiceList(callable $value = null)
{
return $this->getOption()->loadChoiceList($value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, callable $value = null)
{
return $this->getOption()->loadChoicesForValues($values, $value);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, callable $value = null)
{
$this->getOption()->loadValuesForChoices($choices, $value);
}
}

View File

@ -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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_value" callback.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceValue extends AbstractStaticOption
{
}

View File

@ -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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "group_by" callback.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class GroupBy extends AbstractStaticOption
{
}

View File

@ -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\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "preferred_choices" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class PreferredChoice extends AbstractStaticOption
{
}

View File

@ -20,6 +20,7 @@ use Symfony\Contracts\Service\ResetInterface;
* Caches the choice lists created by the decorated factory.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Jules Pietri <jules@heahprod.com>
*/
class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface
{
@ -86,8 +87,13 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterf
$choices = iterator_to_array($choices);
}
// The value is not validated on purpose. The decorated factory may
// decide which values to accept and which not.
// Only cache per value when needed. The value is not validated on purpose.
// The decorated factory may decide which values to accept and which not.
if ($value instanceof Cache\ChoiceValue) {
$value = $value->getOption();
} elseif ($value) {
return $this->decoratedFactory->createListFromChoices($choices, $value);
}
$hash = self::generateHash([$choices, $value], 'fromChoices');
@ -103,6 +109,24 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterf
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
$cache = true;
if ($loader instanceof Cache\ChoiceLoader) {
$loader = $loader->getOption();
} else {
$cache = false;
}
if ($value instanceof Cache\ChoiceValue) {
$value = $value->getOption();
} elseif ($value) {
$cache = false;
}
if (!$cache) {
return $this->decoratedFactory->createListFromLoader($loader, $value);
}
$hash = self::generateHash([$loader, $value], 'fromLoader');
if (!isset($this->lists[$hash])) {
@ -117,8 +141,42 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterf
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
// The input is not validated on purpose. This way, the decorated
// factory may decide which input to accept and which not.
$cache = true;
if ($preferredChoices instanceof Cache\PreferredChoice) {
$preferredChoices = $preferredChoices->getOption();
} elseif ($preferredChoices) {
$cache = false;
}
if ($label instanceof Cache\ChoiceLabel) {
$label = $label->getOption();
} elseif (null !== $label) {
$cache = false;
}
if ($index instanceof Cache\ChoiceFieldName) {
$index = $index->getOption();
} elseif ($index) {
$cache = false;
}
if ($groupBy instanceof Cache\GroupBy) {
$groupBy = $groupBy->getOption();
} elseif ($groupBy) {
$cache = false;
}
if ($attr instanceof Cache\ChoiceAttr) {
$attr = $attr->getOption();
} elseif ($attr) {
$cache = false;
}
if (!$cache) {
return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
}
$hash = self::generateHash([$list, $preferredChoices, $label, $index, $groupBy, $attr]);
if (!isset($this->views[$hash])) {
@ -139,5 +197,6 @@ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterf
{
$this->lists = [];
$this->views = [];
Cache\AbstractStaticOption::reset();
}
}

View File

@ -13,6 +13,13 @@ namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
@ -324,13 +331,13 @@ class ChoiceType extends AbstractType
$resolver->setAllowedTypes('choices', ['null', 'array', '\Traversable']);
$resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']);
$resolver->setAllowedTypes('choice_loader', ['null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface']);
$resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath']);
$resolver->setAllowedTypes('choice_name', ['null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath']);
$resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath']);
$resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath']);
$resolver->setAllowedTypes('preferred_choices', ['array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath']);
$resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath']);
$resolver->setAllowedTypes('choice_loader', ['null', 'Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', ChoiceLoader::class]);
$resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath', ChoiceLabel::class]);
$resolver->setAllowedTypes('choice_name', ['null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath', ChoiceFieldName::class]);
$resolver->setAllowedTypes('choice_value', ['null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath', ChoiceValue::class]);
$resolver->setAllowedTypes('choice_attr', ['null', 'array', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath', ChoiceAttr::class]);
$resolver->setAllowedTypes('preferred_choices', ['array', '\Traversable', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath', PreferredChoice::class]);
$resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', 'Symfony\Component\PropertyAccess\PropertyPath', GroupBy::class]);
}
/**

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Intl\Countries;
use Symfony\Component\OptionsResolver\Options;
@ -29,9 +30,9 @@ class CountryType extends AbstractType
$choiceTranslationLocale = $options['choice_translation_locale'];
$alpha3 = $options['alpha3'];
return new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $alpha3) {
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $alpha3) {
return array_flip($alpha3 ? Countries::getAlpha3Names($choiceTranslationLocale) : Countries::getNames($choiceTranslationLocale));
});
}), [$choiceTranslationLocale, $alpha3]);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Intl\Currencies;
use Symfony\Component\OptionsResolver\Options;
@ -28,9 +29,9 @@ class CurrencyType extends AbstractType
'choice_loader' => function (Options $options) {
$choiceTranslationLocale = $options['choice_translation_locale'];
return new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) {
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) {
return array_flip(Currencies::getNames($choiceTranslationLocale));
});
}), $choiceTranslationLocale);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Intl\Exception\MissingResourceException;
@ -32,7 +33,7 @@ class LanguageType extends AbstractType
$useAlpha3Codes = $options['alpha3'];
$choiceSelfTranslation = $options['choice_self_translation'];
return new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) {
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation) {
if (true === $choiceSelfTranslation) {
foreach (Languages::getLanguageCodes() as $alpha2Code) {
try {
@ -47,7 +48,7 @@ class LanguageType extends AbstractType
}
return array_flip($languagesList);
});
}), [$choiceTranslationLocale, $useAlpha3Codes, $choiceSelfTranslation]);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Intl\Locales;
use Symfony\Component\OptionsResolver\Options;
@ -28,9 +29,9 @@ class LocaleType extends AbstractType
'choice_loader' => function (Options $options) {
$choiceTranslationLocale = $options['choice_translation_locale'];
return new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) {
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($choiceTranslationLocale) {
return array_flip(Locales::getNames($choiceTranslationLocale));
});
}), $choiceTranslationLocale);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\IntlCallbackChoiceLoader;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeZoneToStringTransformer;
@ -49,14 +49,14 @@ class TimezoneType extends AbstractType
if ($options['intl']) {
$choiceTranslationLocale = $options['choice_translation_locale'];
return new IntlCallbackChoiceLoader(function () use ($input, $choiceTranslationLocale) {
return ChoiceList::loader($this, new IntlCallbackChoiceLoader(function () use ($input, $choiceTranslationLocale) {
return self::getIntlTimezones($input, $choiceTranslationLocale);
});
}), [$input, $choiceTranslationLocale]);
}
return new CallbackChoiceLoader(function () use ($input) {
return ChoiceList::lazy($this, function () use ($input) {
return self::getPhpTimezones($input);
});
}, $input);
},
'choice_translation_domain' => false,
'choice_translation_locale' => null,

View File

@ -14,8 +14,12 @@ namespace Symfony\Component\Form\Tests\ChoiceList\Factory;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\FormTypeInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
@ -134,7 +138,7 @@ class CachingFactoryDecoratorTest extends TestCase
$list = new ArrayChoiceList([]);
$closure = function () {};
$this->decoratedFactory->expects($this->once())
$this->decoratedFactory->expects($this->exactly(2))
->method('createListFromChoices')
->with($choices, $closure)
->willReturn($list);
@ -143,6 +147,23 @@ class CachingFactoryDecoratorTest extends TestCase
$this->assertSame($list, $this->factory->createListFromChoices($choices, $closure));
}
public function testCreateFromChoicesSameValueClosureUseCache()
{
$choices = [1];
$list = new ArrayChoiceList([]);
$formType = $this->createMock(FormTypeInterface::class);
$valueCallback = function () {};
$this->decoratedFactory->expects($this->once())
->method('createListFromChoices')
->with($choices, $valueCallback)
->willReturn($list)
;
$this->assertSame($list, $this->factory->createListFromChoices($choices, ChoiceList::value($formType, $valueCallback)));
$this->assertSame($list, $this->factory->createListFromChoices($choices, ChoiceList::value($formType, function () {})));
}
public function testCreateFromChoicesDifferentValueClosure()
{
$choices = [1];
@ -168,14 +189,37 @@ class CachingFactoryDecoratorTest extends TestCase
{
$loader = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')->getMock();
$list = new ArrayChoiceList([]);
$list2 = new ArrayChoiceList([]);
$this->decoratedFactory->expects($this->at(0))
->method('createListFromLoader')
->with($loader)
->willReturn($list)
;
$this->decoratedFactory->expects($this->at(1))
->method('createListFromLoader')
->with($loader)
->willReturn($list2)
;
$this->assertSame($list, $this->factory->createListFromLoader($loader));
$this->assertSame($list2, $this->factory->createListFromLoader($loader));
}
public function testCreateFromLoaderSameLoaderUseCache()
{
$type = $this->createMock(FormTypeInterface::class);
$loader = $this->createMock(ChoiceLoaderInterface::class);
$list = new ArrayChoiceList([]);
$this->decoratedFactory->expects($this->once())
->method('createListFromLoader')
->with($loader)
->willReturn($list);
->willReturn($list)
;
$this->assertSame($list, $this->factory->createListFromLoader($loader));
$this->assertSame($list, $this->factory->createListFromLoader($loader));
$this->assertSame($list, $this->factory->createListFromLoader(ChoiceList::loader($type, $loader)));
$this->assertSame($list, $this->factory->createListFromLoader(ChoiceList::loader($type, $this->createMock(ChoiceLoaderInterface::class))));
}
public function testCreateFromLoaderDifferentLoader()
@ -201,21 +245,53 @@ class CachingFactoryDecoratorTest extends TestCase
public function testCreateFromLoaderSameValueClosure()
{
$loader = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')->getMock();
$type = $this->createMock(FormTypeInterface::class);
$list = new ArrayChoiceList([]);
$list2 = new ArrayChoiceList([]);
$closure = function () {};
$this->decoratedFactory->expects($this->at(0))
->method('createListFromLoader')
->with($loader, $closure)
->willReturn($list)
;
$this->decoratedFactory->expects($this->at(1))
->method('createListFromLoader')
->with($loader, $closure)
->willReturn($list2)
;
$this->assertSame($list, $this->factory->createListFromLoader(ChoiceList::loader($type, $loader), $closure));
$this->assertSame($list2, $this->factory->createListFromLoader(ChoiceList::loader($type, $this->createMock(ChoiceLoaderInterface::class)), $closure));
}
public function testCreateFromLoaderSameValueClosureUseCache()
{
$type = $this->createMock(FormTypeInterface::class);
$loader = $this->createMock(ChoiceLoaderInterface::class);
$list = new ArrayChoiceList([]);
$closure = function () {};
$this->decoratedFactory->expects($this->once())
->method('createListFromLoader')
->with($loader, $closure)
->willReturn($list);
->willReturn($list)
;
$this->assertSame($list, $this->factory->createListFromLoader($loader, $closure));
$this->assertSame($list, $this->factory->createListFromLoader($loader, $closure));
$this->assertSame($list, $this->factory->createListFromLoader(
ChoiceList::loader($type, $loader),
ChoiceList::value($type, $closure)
));
$this->assertSame($list, $this->factory->createListFromLoader(
ChoiceList::loader($type, $this->createMock(ChoiceLoaderInterface::class)),
ChoiceList::value($type, function () {})
));
}
public function testCreateFromLoaderDifferentValueClosure()
{
$loader = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')->getMock();
$type = $this->createMock(FormTypeInterface::class);
$list1 = new ArrayChoiceList([]);
$list2 = new ArrayChoiceList([]);
$closure1 = function () {};
@ -230,8 +306,8 @@ class CachingFactoryDecoratorTest extends TestCase
->with($loader, $closure2)
->willReturn($list2);
$this->assertSame($list1, $this->factory->createListFromLoader($loader, $closure1));
$this->assertSame($list2, $this->factory->createListFromLoader($loader, $closure2));
$this->assertSame($list1, $this->factory->createListFromLoader(ChoiceList::loader($type, $loader), $closure1));
$this->assertSame($list2, $this->factory->createListFromLoader(ChoiceList::loader($type, $this->createMock(ChoiceLoaderInterface::class)), $closure2));
}
public function testCreateViewSamePreferredChoices()
@ -239,14 +315,38 @@ class CachingFactoryDecoratorTest extends TestCase
$preferred = ['a'];
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, $preferred)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, $preferred)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view2, $this->factory->createView($list, $preferred));
}
public function testCreateViewSamePreferredChoicesUseCache()
{
$preferred = ['a'];
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, $preferred)
->willReturn($view);
->willReturn($view)
;
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view, $this->factory->createView($list, ChoiceList::preferred($type, $preferred)));
$this->assertSame($view, $this->factory->createView($list, ChoiceList::preferred($type, ['a'])));
}
public function testCreateViewDifferentPreferredChoices()
@ -275,14 +375,38 @@ class CachingFactoryDecoratorTest extends TestCase
$preferred = function () {};
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, $preferred)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, $preferred)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view2, $this->factory->createView($list, $preferred));
}
public function testCreateViewSamePreferredChoicesClosureUseCache()
{
$preferredCallback = function () {};
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, $preferred)
->willReturn($view);
->with($list, $preferredCallback)
->willReturn($view)
;
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view, $this->factory->createView($list, $preferred));
$this->assertSame($view, $this->factory->createView($list, ChoiceList::preferred($type, $preferredCallback)));
$this->assertSame($view, $this->factory->createView($list, ChoiceList::preferred($type, function () {})));
}
public function testCreateViewDifferentPreferredChoicesClosure()
@ -311,14 +435,38 @@ class CachingFactoryDecoratorTest extends TestCase
$labels = function () {};
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, $labels)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, $labels)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, null, $labels));
$this->assertSame($view2, $this->factory->createView($list, null, $labels));
}
public function testCreateViewSameLabelClosureUseCache()
{
$labelsCallback = function () {};
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, $labels)
->willReturn($view);
->with($list, null, $labelsCallback)
->willReturn($view)
;
$this->assertSame($view, $this->factory->createView($list, null, $labels));
$this->assertSame($view, $this->factory->createView($list, null, $labels));
$this->assertSame($view, $this->factory->createView($list, null, ChoiceList::label($type, $labelsCallback)));
$this->assertSame($view, $this->factory->createView($list, null, ChoiceList::label($type, function () {})));
}
public function testCreateViewDifferentLabelClosure()
@ -347,14 +495,38 @@ class CachingFactoryDecoratorTest extends TestCase
$index = function () {};
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, $index)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, $index)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, null, null, $index));
$this->assertSame($view2, $this->factory->createView($list, null, null, $index));
}
public function testCreateViewSameIndexClosureUseCache()
{
$indexCallback = function () {};
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, $index)
->willReturn($view);
->with($list, null, null, $indexCallback)
->willReturn($view)
;
$this->assertSame($view, $this->factory->createView($list, null, null, $index));
$this->assertSame($view, $this->factory->createView($list, null, null, $index));
$this->assertSame($view, $this->factory->createView($list, null, null, ChoiceList::fieldName($type, $indexCallback)));
$this->assertSame($view, $this->factory->createView($list, null, null, ChoiceList::fieldName($type, function () {})));
}
public function testCreateViewDifferentIndexClosure()
@ -383,14 +555,38 @@ class CachingFactoryDecoratorTest extends TestCase
$groupBy = function () {};
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, null, $groupBy)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, null, $groupBy)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy));
$this->assertSame($view2, $this->factory->createView($list, null, null, null, $groupBy));
}
public function testCreateViewSameGroupByClosureUseCache()
{
$groupByCallback = function () {};
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, null, $groupBy)
->willReturn($view);
->with($list, null, null, null, $groupByCallback)
->willReturn($view)
;
$this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy));
$this->assertSame($view, $this->factory->createView($list, null, null, null, $groupBy));
$this->assertSame($view, $this->factory->createView($list, null, null, null, ChoiceList::groupBy($type, $groupByCallback)));
$this->assertSame($view, $this->factory->createView($list, null, null, null, ChoiceList::groupBy($type, function () {})));
}
public function testCreateViewDifferentGroupByClosure()
@ -419,14 +615,37 @@ class CachingFactoryDecoratorTest extends TestCase
$attr = ['class' => 'foobar'];
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, null, null, $attr)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, null, null, $attr)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr));
}
public function testCreateViewSameAttributesUseCache()
{
$attr = ['class' => 'foobar'];
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, null, null, $attr)
->willReturn($view);
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, ChoiceList::attr($type, $attr)));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, ChoiceList::attr($type, ['class' => 'foobar'])));
}
public function testCreateViewDifferentAttributes()
@ -455,14 +674,37 @@ class CachingFactoryDecoratorTest extends TestCase
$attr = function () {};
$list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock();
$view = new ChoiceListView();
$view2 = new ChoiceListView();
$this->decoratedFactory->expects($this->at(0))
->method('createView')
->with($list, null, null, null, null, $attr)
->willReturn($view)
;
$this->decoratedFactory->expects($this->at(1))
->method('createView')
->with($list, null, null, null, null, $attr)
->willReturn($view2)
;
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view2, $this->factory->createView($list, null, null, null, null, $attr));
}
public function testCreateViewSameAttributesClosureUseCache()
{
$attrCallback = function () {};
$type = $this->createMock(FormTypeInterface::class);
$list = $this->createMock(ChoiceListInterface::class);
$view = new ChoiceListView();
$this->decoratedFactory->expects($this->once())
->method('createView')
->with($list, null, null, null, null, $attr)
->with($list, null, null, null, null, $attrCallback)
->willReturn($view);
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, $attr));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, ChoiceList::attr($type, $attrCallback)));
$this->assertSame($view, $this->factory->createView($list, null, null, null, null, ChoiceList::attr($type, function () {})));
}
public function testCreateViewDifferentAttributesClosure()

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form\Tests\Fixtures;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LazyChoiceTypeExtension extends AbstractTypeExtension
@ -24,7 +24,7 @@ class LazyChoiceTypeExtension extends AbstractTypeExtension
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('choice_loader', new CallbackChoiceLoader(function () {
$resolver->setDefault('choice_loader', ChoiceList::lazy($this, function () {
return [
'Lazy A' => 'lazy_a',
'Lazy B' => 'lazy_b',