diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index d7de810b18068..7beca5c43ec35 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\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 @@ public function configureOptions(OptionsResolver $resolver) $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 @@ public function configureOptions(OptionsResolver $resolver) // 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 @@ public function configureOptions(OptionsResolver $resolver) $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 @@ public function configureOptions(OptionsResolver $resolver) // 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 @@ public function configureOptions(OptionsResolver $resolver) '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 @@ public function getParent() 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 ec8f7933f9a9b..ec51c708aec03 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 @@ public function testLoaderCaching() '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 @@ public function testLoaderCachingWithParameters() '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 24935f0449025..95a3d435b23c0 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 0000000000000..d386f88eba671 --- /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 0000000000000..2f8ac98078ffb --- /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 0000000000000..8de6956d16705 --- /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 0000000000000..0c71e20506d7a --- /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 0000000000000..664a09081f36a --- /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 0000000000000..d8630dd854dbe --- /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 0000000000000..d96f1e9e83b80 --- /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 0000000000000..2ad492caf3923 --- /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 0000000000000..4aefd69ab3e8f --- /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 a217aa5601d73..f7fe8c2465ff1 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php @@ -20,6 +20,7 @@ * Caches the choice lists created by the decorated factory. * * @author Bernhard Schussek + * @author Jules Pietri */ class CachingFactoryDecorator implements ChoiceListFactoryInterface, ResetInterface { @@ -86,8 +87,13 @@ public function createListFromChoices(iterable $choices, $value = null) $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 @@ public function createListFromChoices(iterable $choices, $value = null) */ 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 @@ public function createListFromLoader(ChoiceLoaderInterface $loader, $value = nul */ 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 @@ public function reset() { $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 4be88149770f8..90e973fb7a0bd 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 @@ 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 @@ public function configureOptions(OptionsResolver $resolver) $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 00a19c44f2be2..d2d3aee80aab7 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 @@ public function configureOptions(OptionsResolver $resolver) $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 58136ddb862d9..4506bf488f981 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 @@ public function configureOptions(OptionsResolver $resolver) '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 663e64fa2308c..c5d1ac097740c 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 @@ public function configureOptions(OptionsResolver $resolver) $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 @@ public function configureOptions(OptionsResolver $resolver) } 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 bc6234fd054cb..8c1c2890a0f2e 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 @@ public function configureOptions(OptionsResolver $resolver) '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 0f0157f6beaf3..1aba449665a39 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 @@ public function configureOptions(OptionsResolver $resolver) 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 39d54c536a513..55e01dd206c1d 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 @@ 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 @@ public function testCreateFromChoicesSameValueClosure() $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 @@ public function testCreateFromChoicesSameValueClosure() $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 @@ public function testCreateFromLoaderSameLoader() { $loader = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')->getMock(); $list = new ArrayChoiceList([]); + $list2 = new ArrayChoiceList([]); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createListFromLoader') ->with($loader) - ->willReturn($list); + ->willReturn($list) + ; + $this->decoratedFactory->expects($this->at(1)) + ->method('createListFromLoader') + ->with($loader) + ->willReturn($list2) + ; $this->assertSame($list, $this->factory->createListFromLoader($loader)); - $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) + ; + + $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 @@ public function testCreateFromLoaderDifferentLoader() 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->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createListFromLoader') ->with($loader, $closure) - ->willReturn($list); + ->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->assertSame($list, $this->factory->createListFromLoader($loader, $closure)); - $this->assertSame($list, $this->factory->createListFromLoader($loader, $closure)); + $this->decoratedFactory->expects($this->once()) + ->method('createListFromLoader') + ->with($loader, $closure) + ->willReturn($list) + ; + + $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 @@ public function testCreateFromLoaderDifferentValueClosure() ->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 @@ public function testCreateViewSamePreferredChoices() $preferred = ['a']; $list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock(); $view = new ChoiceListView(); + $view2 = new ChoiceListView(); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createView') ->with($list, $preferred) - ->willReturn($view); + ->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($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) + ; + + $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 @@ public function testCreateViewSamePreferredChoicesClosure() $preferred = function () {}; $list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock(); $view = new ChoiceListView(); + $view2 = new ChoiceListView(); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createView') ->with($list, $preferred) - ->willReturn($view); + ->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($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, $preferredCallback) + ->willReturn($view) + ; + + $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 @@ public function testCreateViewSameLabelClosure() $labels = function () {}; $list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock(); $view = new ChoiceListView(); + $view2 = new ChoiceListView(); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createView') ->with($list, null, $labels) - ->willReturn($view); + ->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($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, $labelsCallback) + ->willReturn($view) + ; + + $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 @@ public function testCreateViewSameIndexClosure() $index = function () {}; $list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock(); $view = new ChoiceListView(); + $view2 = new ChoiceListView(); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createView') ->with($list, null, null, $index) - ->willReturn($view); + ->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($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, $indexCallback) + ->willReturn($view) + ; + + $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 @@ public function testCreateViewSameGroupByClosure() $groupBy = function () {}; $list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock(); $view = new ChoiceListView(); + $view2 = new ChoiceListView(); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createView') ->with($list, null, null, null, $groupBy) - ->willReturn($view); + ->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($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, $groupByCallback) + ->willReturn($view) + ; + + $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 @@ public function testCreateViewSameAttributes() $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 @@ public function testCreateViewSameAttributesClosure() $attr = function () {}; $list = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\ChoiceListInterface')->getMock(); $view = new ChoiceListView(); + $view2 = new ChoiceListView(); - $this->decoratedFactory->expects($this->once()) + $this->decoratedFactory->expects($this->at(0)) ->method('createView') ->with($list, null, null, null, null, $attr) - ->willReturn($view); + ->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($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, $attrCallback) + ->willReturn($view); + + $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 e25d84c8bd748..20fe789cd7dd9 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', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy