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

Skip to content

Commit b8ff42d

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

25 files changed

+950
-89
lines changed

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

Lines changed: 30 additions & 38 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.
@@ -114,44 +115,26 @@ public function configureOptions(OptionsResolver $resolver)
114115
$choiceLoader = function (Options $options) {
115116
// Unless the choices are given explicitly, load them on demand
116117
if (null === $options['choices']) {
117-
$hash = null;
118-
$qbParts = null;
118+
// If there is no QueryBuilder we can safely cache
119+
$vary = [$options['em'], $options['class']];
119120

120-
// If there is no QueryBuilder we can safely cache DoctrineChoiceLoader,
121121
// also if concrete Type can return important QueryBuilder parts to generate
122122
// hash key we go for it as well
123-
if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
124-
$hash = CachingFactoryDecorator::generateHash([
125-
$options['em'],
126-
$options['class'],
127-
$qbParts,
128-
]);
129-
130-
if (isset($this->choiceLoaders[$hash])) {
131-
return $this->choiceLoaders[$hash];
132-
}
133-
}
134-
135-
if (null !== $options['query_builder']) {
136-
$entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']);
137-
} else {
138-
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
139-
$entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']);
123+
if ($options['query_builder'] && null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) {
124+
$vary[] = $qbParts;
140125
}
141126

142-
$doctrineChoiceLoader = new DoctrineChoiceLoader(
127+
return ChoiceList::loader($this, new DoctrineChoiceLoader(
143128
$options['em'],
144129
$options['class'],
145130
$options['id_reader'],
146-
$entityLoader,
147-
false
148-
);
149-
150-
if (null !== $hash) {
151-
$this->choiceLoaders[$hash] = $doctrineChoiceLoader;
152-
}
153-
154-
return $doctrineChoiceLoader;
131+
$this->getCachedEntityLoader(
132+
$options['em'],
133+
$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
134+
$options['class'],
135+
$vary
136+
)
137+
), $vary);
155138
}
156139

157140
return null;
@@ -162,7 +145,7 @@ public function configureOptions(OptionsResolver $resolver)
162145
// field name. We can only use numeric IDs as names, as we cannot
163146
// guarantee that a non-numeric ID contains a valid form name
164147
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) {
165-
return [__CLASS__, 'createChoiceName'];
148+
return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']);
166149
}
167150

168151
// Otherwise, an incrementing integer is used as name automatically
@@ -176,7 +159,7 @@ public function configureOptions(OptionsResolver $resolver)
176159
$choiceValue = function (Options $options) {
177160
// If the entity has a single-column ID, use that ID as value
178161
if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) {
179-
return [$options['id_reader'], 'getIdValue'];
162+
return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']);
180163
}
181164

182165
// Otherwise, an incrementing integer is used as value automatically
@@ -227,8 +210,10 @@ public function configureOptions(OptionsResolver $resolver)
227210
// The reader is cached so that two choice lists for the same class
228211
// (and hence with the same reader) can successfully be cached.
229212
if (!isset($this->idReaders[$hash])) {
230-
$classMetadata = $options['em']->getClassMetadata($options['class']);
231-
$this->idReaders[$hash] = new IdReader($options['em'], $classMetadata);
213+
$this->idReaders[$hash] = new IdReader(
214+
$options['em'],
215+
$options['em']->getClassMetadata($options['class'])
216+
);
232217
}
233218

234219
if ($this->idReaders[$hash]->isSingleId()) {
@@ -243,7 +228,7 @@ public function configureOptions(OptionsResolver $resolver)
243228
'query_builder' => null,
244229
'choices' => null,
245230
'choice_loader' => $choiceLoader,
246-
'choice_label' => [__CLASS__, 'createChoiceLabel'],
231+
'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']),
247232
'choice_name' => $choiceName,
248233
'choice_value' => $choiceValue,
249234
'id_reader' => null, // internal
@@ -273,6 +258,13 @@ public function getParent()
273258

274259
public function reset()
275260
{
276-
$this->choiceLoaders = [];
261+
$this->entityLoaders = [];
262+
}
263+
264+
private function getCachedEntityLoader(ObjectManager $manager, QueryBuilder $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));
277269
}
278270
}

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

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@symfo D7AE ny.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 choice list callbacks or loaders.
28+
*
29+
* @author Jules Pietri <jules@heahprod.com>
30+
*/
31+
final class ChoiceList
32+
{
33+
/**
34+
* Creates a cacheable loader from a callable providing iterable choices.
35+
*
36+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a list
37+
* @param callable $choices A callable that must return iterable 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 list
49+
* @param ChoiceLoaderInterface $loader A loader responsible for creating a cached choice list
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 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" callback to make it cacheable.
71+
*
72+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a list
73+
* @param callable $label Any pseudo callable to create a label from a choice
74+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
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 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" callback to make it cacheable.
95+
*
96+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a list
97+
* @param callable $attr Any pseudo callable to create html attributes from a choice
98+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
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 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" callback to make it cacheable.
119+
*
120+
* @param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a list
121+
* @param callable $preferred Any pseudo callable to return a group name from a choice
122+
* @param mixed|null $vary Dynamic data used to compute a unique hash when caching the callback
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 instantiate.
131+
*/
132+
private function __construct()
133+
{
134+
}
135+
}

src/Symfony/Component/Form/ChoiceList/ChoiceListInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function getValues();
8686
* this method SHOULD be equivalent to {@link getValues()}.
8787
* The $groupBy callback parameter SHOULD be used instead.
8888
*
89-
* @return string[] The choice values
89+
* @return string[]|string[][] The choice values
9090
*/
9191
public function getStructuredValues();
9292

0 commit comments

Comments
 (0)
0