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

Skip to content

Commit 2e0116e

Browse files
HeahDudeJules Pietri
authored and
Jules Pietri
committed
[Form] Added support for caching choice lists based on options
1 parent ad522a9 commit 2e0116e

25 files changed

+962
-101
lines changed

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

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

4344
/**
44-
* @var DoctrineChoiceLoader[]
45+
* @var EntityLoaderInterface[]
4546
*/
46-
private $choiceLoaders = [];
47+
private $entityLoaders = [];
4748

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

119-
// If there is no QueryBuilder we can safely cache DoctrineChoiceLoader,
120120
// also if concrete Type can return important QueryBuilder parts to generate
121121
// hash key we go for it as well
122-
if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
123-
$hash = CachingFactoryDecorator::generateHash([
124-
$options['em'],
125-
$options['class'],
126-
$qbParts,
127-
]);
128-
129-
if (isset($this->choiceLoaders[$hash])) {
130-
return $this->choiceLoaders[$hash];
131-
}
122+
if ($options['query_builder']) {
123+
$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
132124
}
133125

134-
if (null !== $options['query_builder']) {
135-
$entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']);
136-
} else {
137-
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
138-
$entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']);
139-
}
140-
141-
$doctrineChoiceLoader = new DoctrineChoiceLoader(
126+
return ChoiceList::loader($this, new DoctrineChoiceLoader(
142127
$options['em'],
143128
$options['class'],
144129
$options['id_reader'],
145-
$entityLoader
146-
);
147-
148-
if (null !== $hash) {
149-
$this->choiceLoaders[$hash] = $doctrineChoiceLoader;
150-
}
151-
152-
return $doctrineChoiceLoader;
130+
$this->getCachedEntityLoader(
131+
$options['em'],
132+
$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
133+
$options['class'],
134+
$vary
135+
)
136+
), $vary);
153137
}
154138

155139
return null;
@@ -160,7 +144,7 @@ public function configureOptions(OptionsResolver $resolver)
160144
// field name. We can only use numeric IDs as names, as we cannot
161145
// guarantee that a non-numeric ID contains a valid form name
162146
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
163- return [__CLASS__, 'createChoiceName'];
147+
return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName'], $options['id_reader']);
164148
}
165149

166150
// Otherwise, an incrementing integer is used as name automatically
@@ -174,7 +158,7 @@ public function configureOptions(OptionsResolver $resolver)
174158
$choiceValue = function (Options $options) {
175159
// If the entity has a single-column ID, use that ID as value
176160
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
177-
return [$options['id_reader'], 'getIdValue'];
161+
return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']);
178162
}
179163

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

239209
$resolver->setDefaults([
240210
'em' => null,
241211
'query_builder' => null,
242212
'choices' => null,
243213
'choice_loader' => $choiceLoader,
244-
'choice_label' => [__CLASS__, 'createChoiceLabel'],
214+
'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
245215
'choice_name' => $choiceName,
246216
'choice_value' => $choiceValue,
247217
'id_reader' => null, // internal
@@ -271,6 +241,28 @@ public function getParent()
271241

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

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,11 @@ public function testSubmitSingleNonExpandedStringCastableIdentifier()
581581
$this->assertSame('2', $field->getViewData());
582582
}
583583

584+
/**
585+
* @group legacy
586+
*
587+
* @expectedDeprecation Implicitly cached choice lists are deprecated since 5.1. Use one of "Symfony\Component\Form\ChoiceList\ChoiceList" methods instead to define choice options as of 6.0.
588+
*/
584589
public function testSubmitSingleStringCastableIdentifierExpanded()
585590
{
586591
$entity1 = new SingleStringCastableIdEntity(1, 'Foo');
@@ -662,6 +667,11 @@ public function testSubmitMultipleNonExpandedStringCastableIdentifier()
662667
$this->assertSame(['1', '3'], $field->getViewData());
663668
}
664669

670+
/**
671+
* @group legacy
672+
*
673+
* @expectedDeprecation Implicitly cached choice lists are deprecated since 5.1. Use one of "Symfony\Component\Form\ChoiceList\ChoiceList" methods instead to define choice options as of 6.0.
674+
*/
665675
public function testSubmitMultipleStringCastableIdentifierExpanded()
666676
{
667677
$entity1 = new SingleStringCastableIdEntity(1, 'Foo');
@@ -716,6 +726,11 @@ public function testOverrideChoices()
716726
$this->assertSame('2', $field->getViewData());
717727
}
718728

729+
/**
730+
* @group legacy
731+
*
732+
* @expectedDeprecation Implicitly cached choice lists are deprecated since 5.1. No list will be cached in 6.0, use "Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue" instead to define the choice value.
733+
*/
719734
public function testOverrideChoicesValues()
720735
{
721736
$entity1 = new SingleIntIdEntity(1, 'Foo');
@@ -738,6 +753,11 @@ public function testOverrideChoicesValues()
738753
$this->assertSame('Bar', $field->getViewData());
739754
}
740755

756+
/**
757+
* @group legacy
758+
*
759+
* @expectedDeprecation Implicitly cached choice lists are deprecated since 5.1. No list will be cached in 6.0, use "Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceValue" instead to define the choice value.
760+
*/
741761
public function testOverrideChoicesValuesWithCallab 10000 le()
742762
{
743763
$entity1 = new GroupableEntity(1, 'Foo', 'BazGroup');
@@ -1155,13 +1175,13 @@ public function testLoaderCaching()
11551175
'property3' => 2,
11561176
]);
11571177

1158-
$choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader');
1159-
$choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader');
1160-
$choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader');
1178+
$choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list');
1179+
$choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list');
1180+
$choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list');
11611181

1162-
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1);
1163-
$this->assertSame($choiceLoader1, $choiceLoader2);
1164-
$this->assertSame($choiceLoader1, $choiceLoader3);
1182+
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1);
1183+
$this->assertSame($choiceList1, $choiceList2);
1184+
$this->assertSame($choiceList1, $choiceList3);
11651185
}
11661186

11671187
public function testLoaderCachingWithParameters()
@@ -1215,13 +1235,13 @@ public function testLoaderCachingWithParameters()
12151235
'property3' => 2,
12161236
]);
12171237

1218-
$choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader');
1219-
$choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader');
1220-
$choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader');
1238+
$choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list');
1239+
$choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list');
1240+
$choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list');
12211241

1222-
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1);
1223-
$this->assertSame($choiceLoader1, $choiceLoader2);
1224-
$this->assertSame($choiceLoader1, $choiceLoader3);
1242+
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1);
1243+
$this->assertSame($choiceList1, $choiceList2);
1244+
$this->assertSame($choiceList1, $choiceList3);
12251245
}
12261246

12271247
protected function createRegistryMock($name, $em)

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"symfony/stopwatch": "^4.4|^5.0",
2828
"symfony/config": "^4.4|^5.0",
2929
"symfony/dependency-injection": "^4.4|^5.0",
30-
"symfony/form": "^5.0",
30+
"symfony/form": "^5.1",
3131
"symfony/http-kernel": "^5.0",
3232
"symfony/messenger": "^4.4|^5.0",
3333
"symfony/property-access": "^4.4|^5.0",

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.1.0
5+
-----
6+
7+
* added a `ChoiceList` facade to leverage explicit choice list caching based on callback options or specific loaders
8+
49
5.0.0
510
-----
611

0 commit comments

Comments
 (0)
0