From b25973cc2ea9fbf1afdc0ab46ae7d71098eb7304 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Sun, 7 Apr 2019 22:07:24 +0200 Subject: [PATCH] [Form] Added support for caching choice lists based on options --- .../Doctrine/Form/Type/DoctrineType.php | 93 +++--- .../Tests/Form/Type/EntityTypeTest.php | 24 +- src/Symfony/Component/Form/CHANGELOG.md | 1 + .../Component/Form/ChoiceList/ChoiceList.php | 135 ++++++++ .../Factory/Cache/AbstractStaticOption.php | 64 ++++ .../ChoiceList/Factory/Cache/ChoiceAttr.php | 27 ++ .../Factory/Cache/ChoiceFieldName.php | 27 ++ .../ChoiceList/Factory/Cache/ChoiceLabel.php | 27 ++ .../ChoiceList/Factory/Cache/ChoiceLoader.php | 51 +++ .../ChoiceList/Factory/Cache/ChoiceValue.php | 27 ++ .../Form/ChoiceList/Factory/Cache/GroupBy.php | 27 ++ .../Factory/Cache/PreferredChoice.php | 27 ++ .../Factory/CachingFactoryDecorator.php | 67 +++- .../Form/Extension/Core/Type/ChoiceType.php | 21 +- .../Form/Extension/Core/Type/CountryType.php | 5 +- .../Form/Extension/Core/Type/CurrencyType.php | 5 +- .../Form/Extension/Core/Type/LanguageType.php | 5 +- .../Form/Extension/Core/Type/LocaleType.php | 5 +- .../Form/Extension/Core/Type/TimezoneType.php | 10 +- .../Factory/CachingFactoryDecoratorTest.php | 308 ++++++++++++++++-- .../Fixtures/LazyChoiceTypeExtension.php | 4 +- 21 files changed, 838 insertions(+), 122 deletions(-) create mode 100644 src/Symfony/Component/Form/ChoiceList/ChoiceList.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/AbstractStaticOption.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceAttr.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceFieldName.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLabel.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLoader.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceValue.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/GroupBy.php create mode 100644 src/Symfony/Component/Form/ChoiceList/Factory/Cache/PreferredChoice.php diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index d7de810b18..7beca5c43e 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -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)); } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index ec8f7933f9..ec51c708ae 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -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) diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 24935f0449..95a3d435b2 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -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. diff --git a/src/Symfony/Component/Form/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ChoiceList.php new file mode 100644 index 0000000000..d386f88eba --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/ChoiceList.php @@ -0,0 +1,135 @@ + + * + * 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 + */ +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() + { + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/AbstractStaticOption.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/AbstractStaticOption.php new file mode 100644 index 0000000000..2f8ac98078 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/AbstractStaticOption.php @@ -0,0 +1,64 @@ + + * + * 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 + */ +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 = []; + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceAttr.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceAttr.php new file mode 100644 index 0000000000..8de6956d16 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceAttr.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +final class ChoiceAttr extends AbstractStaticOption +{ +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceFieldName.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceFieldName.php new file mode 100644 index 0000000000..0c71e20506 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceFieldName.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +final class ChoiceFieldName extends AbstractStaticOption +{ +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLabel.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLabel.php new file mode 100644 index 0000000000..664a09081f --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLabel.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +final class ChoiceLabel extends AbstractStaticOption +{ +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLoader.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLoader.php new file mode 100644 index 0000000000..d8630dd854 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceLoader.php @@ -0,0 +1,51 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceValue.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceValue.php new file mode 100644 index 0000000000..d96f1e9e83 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/ChoiceValue.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +final class ChoiceValue extends AbstractStaticOption +{ +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/GroupBy.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/GroupBy.php new file mode 100644 index 0000000000..2ad492caf3 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/GroupBy.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +final class GroupBy extends AbstractStaticOption +{ +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/Cache/PreferredChoice.php b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/PreferredChoice.php new file mode 100644 index 0000000000..4aefd69ab3 --- /dev/null +++ b/src/Symfony/Component/Form/ChoiceList/Factory/Cache/PreferredChoice.php @@ -0,0 +1,27 @@ + + * + * 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 + */ +final class PreferredChoice extends AbstractStaticOption +{ +} diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php index a217aa5601..f7fe8c2465 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -20,6 +20,7 @@ use Symfony\Contracts\Service\ResetInterface; * Caches the choice lists created by the decorated factory. * * @author Bernhard Schussek + * @author Jules Pietri */ 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(); } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 4be8814977..90e973fb7a 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -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]); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php index 00a19c44f2..d2d3aee80a 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CountryType.php @@ -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, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php b/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php index 58136ddb86..4506bf488f 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CurrencyType.php @@ -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, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php index 663e64fa23..c5d1ac0977 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/LanguageType.php @@ -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, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php index bc6234fd05..8c1c2890a0 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/LocaleType.php @@ -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, diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php index 0f0157f6be..1aba449665 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TimezoneType.php @@ -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, diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php index 39d54c536a..55e01dd206 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/CachingFactoryDecoratorTest.php @@ -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 @@ -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() diff --git a/src/Symfony/Component/Form/Tests/Fixtures/LazyChoiceTypeExtension.php b/src/Symfony/Component/Form/Tests/Fixtures/LazyChoiceTypeExtension.php index e25d84c8bd..20fe789cd7 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/LazyChoiceTypeExtension.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/LazyChoiceTypeExtension.php @@ -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',