8000 [Form] Added support for caching choice lists based on options · symfony/symfony@1700f87 · 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 1700f87

Browse files
committed
[Form] Added support for caching choice lists based on options
1 parent ae00ff4 commit 1700f87

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.
@@ -116,43 +117,26 @@ public function configureOptions(OptionsResolver $resolver)
116117
$choiceLoader = function (Options $options) {
117118
// Unless the choices are given explicitly, load them on demand
118119
if (null === $options['choices']) {
119-
$hash = null;
120-
$qbParts = null;
120+
// If there is no QueryBuilder we can safely cache
121+
$vary = [$options['em'], $options['class']];
121122

122-
// If there is no QueryBuilder we can safely cache DoctrineChoiceLoader,
123123
// also if concrete Type can return important QueryBuilder parts to generate
124124
// hash key we go for it as well
125-
if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
126-
$hash = CachingFactoryDecorator::generateHash([
127-
$options['em'],
128-
$options['class F438 9;],
129-
$qbParts,
130-
]);
131-
132-
if (isset($this->choiceLoaders[$hash])) {
133-
return $this->choiceLoaders[$hash];
134-
}
125+
if ($options['query_builder'] && null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
126+
$vary[] = $qbParts;
135127
}
136128

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

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

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

183167
// Otherwise, an incrementing integer is used as value automatically
@@ -216,35 +200,21 @@ public function configureOptions(OptionsResolver $resolver)
216200
// Set the "id_reader" option via the normalizer. This option is not
217201
// supposed to be set by the user.
218202
$idReaderNormalizer = function (Options $options) {
219-
$hash = CachingFactoryDecorator::generateHash([
220-
$options['em'],
221-
$options['class'],
222-
]);
223-
224203
// The ID reader is a utility that is needed to read the object IDs
225204
// when generating the field values. The callback generating the
226205
// field values has no access to the object manager or the class
227206
// of the field, so we store that information in the reader.
228207
// The reader is cached so that two choice lists for the same class
229208
// (and hence with the same reader) can successfully be cached.
230-
if (!isset($this->idReaders[$hash])) {
231-
$classMetadata = $options['em']->getClassMetadata($options['class']);
232-
$this->idReaders[$hash] = new IdReader($options['em'], $classMetadata);
233-
}
234-
235-
if ($this->idReaders[$hash]->isSingleId()) {
236-
return $this->idReaders[$hash];
237-
}
238-
239-
return null;
209+
return $this->getCachedIdReader($options['em'], $options['class']);
240210
};
241211

242212
$resolver->setDefaults([
243213
'em' => null,
244214
'query_builder' => null,
245215
'choices' => null,
246216
'choice_loader' => $choiceLoader,
247-
'choice_label' => [__CLASS__, 'createChoiceLabel'],
217+
'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
248218
'choice_name' => $choiceName,
249219
'choice_value' => $choiceValue,
250220
'id_reader' => null, // internal
@@ -276,6 +246,28 @@ public function getParent()
276246

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

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 testOverrideChoicesValuesWithCallable()
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