Skip to content

Commit c379ca7

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

25 files changed

+950
-88
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@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 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy