8000 [Form] Added support for caching choice lists based on options · symfony/symfony@bbb4797 · GitHub
[go: up one dir, main page]

Skip to content
Sign in

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit bbb4797

Browse files
committed
[Form] Added support for caching choice lists based on options
1 parent fcb833f commit bbb4797

21 files changed

+836
-119
lines changed

src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
2121
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
2222
use Symfony\Component\Form\AbstractType;
23+
use Symfony\Component\Form\ChoiceList\ChoiceList;
2324
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
2425
use Symfony\Component\Form\Exception\RuntimeException;
2526
use Symfony\Component\Form\FormBuilderInterface;
@@ -40,9 +41,9 @@ abstract class DoctrineType extends AbstractType implements ResetInterface
4041
private $idReaders = [];
4142

4243
/**
43-
* @var DoctrineChoiceLoader[]
44+
* @var EntityLoaderInterface[]
4445
*/
45-
private $choiceLoaders = [];
46+
private $entityLoaders = [];
4647

4748
/**
4849
* Creates the label for a choice.
@@ -115,43 +116,26 @@ public function configureOptions(OptionsResolver $resolver)
115116
$choiceLoader = function (Options $options) {
116117
// Unless the choices are given explicitly, load them on demand
117118
if (null === $options['choices']) {
118-
$hash = null;
119-
$qbParts = null;
119+
// If there is no QueryBuilder we can safely cache
120+
$vary = [$options['em'], $options['class']];
120121

121-
// If there is no QueryBuilder we can safely cache DoctrineChoiceLoader,
122122
// also if concrete Type can return important QueryBuilder parts to generate
123-
// hash key we go for it as well
124-
if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
125-
$hash = CachingFactoryDecorator::generateHash([
126-
$options['em'],
127-
$options['class'],
128-
$qbParts,
129-
]);
130-
131-
if (isset($this->choiceLoaders[$hash])) {
132-
return $this->choiceLoaders[$hash];
133-
}
123+
// hash key we go for it as well, otherwise fallback on the instance
124+
if ($options['query_builder']) {
125+
$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
134126
}
135127

136-
if (null !== $options['query_builder']) {
137-
$entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']);
138-
} else {
139-
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
140-
$entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']);
141-
}
142-
143-
$doctrineChoiceLoader = new DoctrineChoiceLoader(
128+
return ChoiceList::loader($this, new DoctrineChoiceLoader(
144129
$options['em'],
145130
$options['class'],
146131
$options['id_reader'],
147-
$entityLoader
148-
);
149-
150-
if (null !== $hash) {
151-
$this->choiceLoaders[$hash] = $doctrineChoiceLoader;
152-
}
153-
154-
return $doctrineChoiceLoader;
132+
$this->getCachedEntityLoader(
133+
$options['em'],
134+
$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
135+
$options['class'],
136+
$vary
137+
)
138+
), $vary);
155139
}
156140

157141
return null;
@@ -162,7 +146,7 @@ public function configureOptions(OptionsResolver $resolver)
162146
// field name. We can only use numeric IDs as names, as we cannot
163147
// guarantee that a non-numeric ID contains a valid form name
164148
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
165-
return [__CLASS__, 'createChoiceName'];
149+
return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);
166150
}
167151

168152
// Otherwise, an incrementing integer is used as name automatically
@@ -176,7 +160,7 @@ public function configureOptions(OptionsResolver $resolver)
176160
$choiceValue = function (Options $options) {
177161
// If the entity has a single-column ID, use that ID as value
178162
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
179-
return [$options['id_reader'], 'getIdValue'];
163+
return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']);
180164
}
181165

182166
// Otherwise, an incrementing integer is used as value automatically
@@ -214,35 +198,21 @@ public function configureOptions(OptionsResolver $resolver)
214198
// Set the "id F987 _reader" option via the normalizer. This option is not
215199
// supposed to be set by the user.
216200
$idReaderNormalizer = function (Options $options) {
217-
$hash = CachingFactoryDecorator::generateHash([
218-
$options['em'],
219-
$options['class'],
220-
]);
221-
222201
// The ID reader is a utility that is needed to read the object IDs
223202
// when generating the field values. The callback generating the
224203
// field values has no access to the object manager or the class
225204
// of the field, so we store that information in the reader.
226205
// The reader is cached so that two choice lists for the same class
227206
// (and hence with the same reader) can successfully be cached.
228-
if (!isset($this->idReaders[$hash])) {
229-
$classMetadata = $options['em']->getClassMetadata($options['class']);
230-
$this->idReaders[$hash] = new IdReader($options['em'], $classMetadata);
231-
}
232-
233-
if ($this->idReaders[$hash]->isSingleId()) {
234-
return $this->idReaders[$hash];
235-
}
236-
237-
return null;
207+
return $this->getCachedIdReader($options['em'], $options['class']);
238208
};
239209

240210
$resolver->setDefaults([
241211
'em' => null,
242212
'query_builder' => null,
243213
'choices' => null,
244214
'choice_loader' => $choiceLoader,
245-
'choice_label' => [__CLASS__, 'createChoiceLabel'],
215+
'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
246216
'choice_name' => $choiceName,
247217
'choice_value' => $choiceValue,
248218
' 1241 id_reader' => null, // internal
@@ -274,6 +244,27 @@ public function getParent()
274244

275245
public function reset()
276246
{
277-
$this->choiceLoaders = [];
247+
$this->entityLoaders = [];
248+
}
249+
250+
private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader
251+
{
252+
$hash = CachingFactoryDecorator::generateHash([$manager, $class]);
253+
254+
if (isset($this->idReaders[$hash])) {
255+
return $this->idReaders[$hash];
256+
}
257+
258+
$idReader = new IdReader($manager, $manager->getClassMetadata($class));
259+
260+
// don't cache the instance for composite ids that cannot be optimized
261+
return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null;
262+
}
263+
264+
private function getCachedEntityLoader(ObjectManager $manager, $queryBuilder, string $class, array $vary): EntityLoaderInterface
265+
{
266+
$hash = CachingFactoryDecorator::generateHash($vary);
267+
268+
return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class));
278269
}
279270
}

src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,13 +1205,13 @@ public function testLoaderCaching()
12051205
'property3' => 2,
12061206
]);
12071207

1208-
$choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader');
1209-
$choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader');
1210-
$choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader');
1208+
$choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list');
1209+
$choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list');
1210+
$choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list');
12111211

1212-
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1);
1213-
$this->assertSame($choiceLoader1, $choiceLoader2);
1214-
$this->assertSame($choiceLoader1, $choiceLoader3);
1212+
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1);
1213+
$this->assertSame($choiceList1, $choiceList2);
1214+
$this->assertSame($choiceList1, $choiceList3);
12151215
}
12161216

12171217
public function testLoaderCachingWithParameters()
@@ -1265,13 +1265,13 @@ public function testLoaderCachingWithParameters()
12651265
'property3' => 2,
12661266
]);
12671267

1268-
$choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader');
1269-
$choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader');
1270-
$choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader');
1268+
$choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list');
1269+
$choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list');
1270+
$choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list');
12711271

1272-
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1);
1273-
$this->assertSame($choiceLoader1, $choiceLoader2);
1274-
$this->assertSame($choiceLoader1, $choiceLoader3);
1272+
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1);
1273+
$this->assertSame($choiceList1, $choiceList2);
1274+
$this->assertSame($choiceList1, $choiceList3);
12751275
}
12761276

12771277
protected function createRegistryMock($name, $em)

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.1.0
55
-----
66

7+
* Added a `ChoiceList` facade to leverage explicit choice list caching based on options
78
* Added an `AbstractChoiceLoader` to simplify implementations and handle global optimizations
89
* The `view_timezone` option defaults to the `model_timezone` if no `reference_date` is configured.
910
* Added default `inputmode` attribute to Search, Email and Tel form types.
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\ChoiceList;
13+
14+
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceAttr;
15+
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceFieldName;
16+
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLabel;
17+
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
18+
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue;
19+
use Symfony\Component\Form\ChoiceList\Factory\Cache\GroupBy;
20+
use Symfony\Component\Form\ChoiceList\Factory\Cache\PreferredChoice;
21+
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
22+
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
23+
use Symfony\Component\Form\FormTypeExtensionInterface;
24+
use Symfony\Component\Form\FormTypeInterface;
25+
26+
/**
27+
* A set of convenient static methods to create cacheable choice list options.
28+
*
29+
* @author Jules Pietri <jules@heahprod.com>
30+
*/
31+
final class ChoiceList
32+
{
33+
/**
34+
* Creates a cacheable loader from any callable providing iterable choices.
35+
*
36+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
37+
* @param callable $choices A callable that must return iterable choices or grouped choices
38+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader
39+
*/
40+
public static function lazy($formType, callable $choices, $vary = null): ChoiceLoader
41+
{
42+
return self::loader($formType, new CallbackChoiceLoader($choices), $vary);
43+
}
44+
45+
/**
46+
* Decorates a loader to make it cacheable.
47+
*
48+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
49+
* @param ChoiceLoaderInterface $loader A loader responsible for creating loading choices or grouped choices
50+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the loader
51+
*/
52+
public static function loader($formType, ChoiceLoaderInterface $loader, $vary = null): ChoiceLoader
53+
{
54+
return new ChoiceLoader($formType, $loader, $vary);
55+
}
56+
57+
/**
58+
* Decorates a "choice_value" callback to make it cacheable.
59+
*
60+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
61+
* @param callable $value Any pseudo callable to create a unique string value from a choice
62+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
63+
*/
64+
public static function value($formType, $value, $vary = null): ChoiceValue
65+
{
66+
return new ChoiceValue($formType, $value, $vary);
67+
}
68+
69+
/**
70+
* Decorates a "choice_label" option to make it cacheable.
71+
*
72+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
73+
* @param callable|false $label Any pseudo callable to create a label from a choice or false to discard it
74+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
75+
*/
76+
public static function label($formType, $label, $vary = null): ChoiceLabel
77+
{
78+
return new ChoiceLabel($formType, $label, $vary);
79+
}
80+
81+
/**
82+
* Decorates a "choice_name" callback to make it cacheable.
83+
*
84+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
85+
* @param callable $fieldName Any pseudo callable to create a field name from a choice
86+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
87+
*/
88+
public static function fieldName($formType, $fieldName, $vary = null): ChoiceFieldName
89+
{
90+
return new ChoiceFieldName($formType, $fieldName, $vary);
91+
}
92+
93+
/**
94+
* Decorates a "choice_attr" option to make it cacheable.
95+
*
96+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
97+
* @param callable|array $attr Any pseudo callable or array to create html attributes from a choice
98+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
99+
*/
100+
public static function attr($formType, $attr, $vary = null): ChoiceAttr
101+
{
102+
return new ChoiceAttr($formType, $attr, $vary);
103+
}
104+
105+
/**
106+
* Decorates a "group_by" callback to make it cacheable.
107+
*
108+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
109+
* @param callable $groupBy Any pseudo callable to return a group name from a choice
110+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
111+
*/
112+
public static function groupBy($formType, $groupBy, $vary = null): GroupBy
113+
{
114+
return new GroupBy($formType, $groupBy, $vary);
115+
}
116+
117+
/**
118+
* Decorates a "preferred_choices" option to make it cacheable.
119+
*
120+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
121+
* @param callable|array $preferred Any pseudo callable or array to return a group name from a choice
122+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
123+
*/
124+
public static function preferred($formType, $preferred, $vary = null): PreferredChoice
125+
{
126+
return new PreferredChoice($formType, $preferred, $vary);
127+
}
128+
129+
/**
130+
* Should not be instantiated.
131+
*/
132+
private function __construct()
133+
{
134+
}
135+
}

0 commit comments

Comments
 (0)
0