feature #30994 [Form] Added support for caching choice lists based on options (HeahDude)

This PR was merged into the 5.1-dev branch.

Discussion
----------

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

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | ~
| License       | MIT
| Doc PR        | symfony/symfony-docs#13182

Currently, the `CachingFactoryDecorator` is responsible for unnecessary memory usage, anytime a choice option is set with a callback option defined as an anonymous function or a loader, then a new hash is generated for the choice list, while we may expect the list to be reused once "finally" configured in a form type or choice type extension.

A simple case is when using one of the core intl choice types in a collection:
```php
// ...
class SomeFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('some_choices', ChoiceType::class, [
                // before: cached choice list (unnecessary overhead)
                // after: no cache (better perf)
                'choices' => $someObjects,
                'choice_value' => function (?object $choice) { /* return some string */ },
            ])

            // see below the nested effects
            ->add('nested_fields', CollectionType::class, [
                'entry_type' => NestedFormType::class,
            ])
    // ...
}

// ...
class NestedFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('some_other_choices', ChoiceType::class, [
                // before: cached choice list for every entry because we define a new closure instance for each field
                // after: no cache, a bit better for the same result, but much better if it were not nested in a collection
                'choices' => $someOtherObjects,
                'choice_value' => function (?object $otherChoice) { /* return some string */ },
            ])

            ->add('some_loaded_choices', ChoiceType::class, [
                // before: cached but for every entry since every field will have its
                //         own instance of loader, generating a new hash
                // after: no cache, same pro/cons as above
                'choice_loader' => new CallbackChoiceLoader(function() { /* return some choices */}),
                // or
                'choice_loader' => new SomeLoader(),
            ])

            ->add('person', EntityType::class, [
                // before: cached but for every entry, because we define extra `choice_*` option
                // after: no cache, same pro/cons as above
                'class' => SomeEntity::class,
                'choice_label' => function (?SomeEntity $choice) { /* return some label */},
            ])

            // before: cached for every entry, because the type define some "choice_*" option
            // after: cached only once, better perf since the same loader is used for every entry
            ->add('country', CountryType::class)

            // before: cached for every entry, because the type define some "choice_*" option
            // after: no cache, same pro/cons as above
            ->add('locale', LocaleType::class, [
                'preferred_choices' => [ /* some preferred locales */ ],
                'group_by' => function (?string $locale, $label) { /* return some group */ },
            ])
// ...
```

In such cases, we would expect every entries to use the same cached intl choice list, but not, as many list and views as entries will be kept in cache. This is even worse if some callback options like `choice_label`, `choice_value`, `choice_attr`, `choice_name`, `preferred_choices` or `group_by` are used.
This PR helps making cache explicit when needed and ~deprecate~ drop unexpected implicit caching of choice list for most simple cases responsible of unnecessary overhead.

The result is better performance just by upgrading to 5.1 \o/.
But to solve the cases above when cache is needed per options, one should now use the new `ChoiceList` static methods to wrap option values, which is already done internally in this PR.

```php
use Symfony\Component\Form\ChoiceList\ChoiceList;
// ...
class NestedFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // explicitly shared cached choice lists between entries

            ->add('some_other_choices', ChoiceType::class, [
                'choices' => $someOtherObjects,
                'choice_value' => ChoiceList::value($this, function (?object $otherChoice) {
                    /* return some string */
                }),
            ]),

            ->add('some_loaded_choices', ChoiceType::class, [
                'choice_loader' => ChoiceList::lazy($this, function() {
                    /* return some choices */
                })),
                // or
                'choice_loader' => ChoiceList::loader($this, new SomeLoader()),
            ]),

            ->add('person', EntityType::class, [
                'class' => SomeEntity::class,
                'choice_label' => ChoiceList::label($this, function (?SomeEntity $choice) {
                    /* return some label */
                },
            ])

            // nothing to do :)
            ->add('country', CountryType::class)

            ->add('locale', LocaleType::class, [
                'preferred_choices' => ChoiceList::preferred($this, [ /* some preferred locales */ ]),
                'group_by' => ChoiceList::groupBy($this, function (?string $locale, $label) {
                    /* return some group */
                }),
            ])
// ...
```

I've done some nice profiling with Blackfire and the simple example above in a fresh website skeleton and only two empty entries as initial data, then submitting an empty form. That gives the following results:

 * Rendering the form - Before vs After

  <img width="714" alt="Screenshot 2020-02-16 at 9 24 58 PM" src="https://user-images.githubusercontent.com/10107633/74612132-de533180-5102-11ea-9cc4-296a16949d90.png">

 * Rendering the form - Before vs After with `ChoiceList` helpers

  <img width="670" alt="Screenshot 2020-02-16 at 9 26 51 PM" src="https://user-images.githubusercontent.com/10107633/74612155-122e5700-5103-11ea-9c16-5d80a7541f4b.png">

 * Submitting the form - Before vs After

  <img width="670" alt="Screenshot 2020-02-16 at 9 28 01 PM" src="https://user-images.githubusercontent.com/10107633/74612172-3be77e00-5103-11ea-9a18-4294e05402d2.png">

 * Submitting the form - Before vs After with `ChoiceList` helpers

  <img width="670" alt="Screenshot 2020-02-16 at 9 29 10 PM" src="https://user-images.githubusercontent.com/10107633/74612193-689b9580-5103-11ea-86b9-5b4906200021.png">

_________

TODO:
- [x] Docs
- [x] More profiling
- [x] Add some tests

#EUFOSSA

Commits
-------

b25973cc2e [Form] Added support for caching choice lists based on options
This commit is contained in:
Fabien Potencier 2020-02-21 08:55:09 +01:00
commit 269c4a2e15
21 changed files with 838 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_attr" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceAttr extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_name" callback.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceFieldName extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_label" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceLabel extends AbstractStaticOption
{
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_loader" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInterface
{
/**
* {@inheritdoc}
*/
public function loadChoiceList(callable $value = null)
{
return $this->getOption()->loadChoiceList($value);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, callable $value = null)
{
return $this->getOption()->loadChoicesForValues($values, $value);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, callable $value = null)
{
$this->getOption()->loadValuesForChoices($choices, $value);
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "choice_value" callback.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class ChoiceValue extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "group_by" callback.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class GroupBy extends AbstractStaticOption
{
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\ChoiceList\Factory\Cache;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeInterface;
/**
* A cacheable wrapper for any {@see FormTypeInterface} or {@see FormTypeExtensionInterface}
* which configures a "preferred_choices" option.
*
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class PreferredChoice extends AbstractStaticOption
{
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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