8000 add ExtraLazyChoiceLoader and extra_lazy option · symfony/symfony@87afdea · GitHub
[go: up one dir, main page]

Skip to content

Commit 87afdea

Browse files
committed
add ExtraLazyChoiceLoader and extra_lazy option
1 parent c9e7809 commit 87afdea

File tree

7 files changed

+233
-6
lines changed

7 files changed

+233
-6
lines changed

src/Symfony/Bridge/Doctrine/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+
7.1
5+
---
6+
7+
* Add `extra_lazy` option to `DoctrineType` for loading entities on demand
8+
49
7.0
510
---
611

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Form\AbstractType;
2323
use Symfony\Component\Form\ChoiceList\ChoiceList;
2424
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
25+
use Symfony\Component\Form\ChoiceList\Loader\ExtraLazyChoiceLoader;
2526
use Symfony\Component\Form\Exception\RuntimeException;
2627
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
2728
use Symfony\Component\Form\FormBuilderInterface;
@@ -118,17 +119,23 @@ public function configureOptions(OptionsResolver $resolver): void
118119
$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder'];
119120
}
120121

121-
return ChoiceList::loader($this, new DoctrineChoiceLoader(
122+
$loader = new DoctrineChoiceLoader(
122123
$options['em'],
123124
$options['class'],
124125
$options['id_reader'],
125126
$this->getCachedEntityLoader(
126127
$options['em'],
127128
$options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'),
128129
$options['class'],
129-
$vary
130-
)
131-
), $vary);
130+
$vary,
131+
),
132+
);
133+
134+
if ($options['extra_lazy']) {
135+
$loader = new ExtraLazyChoiceLoader($loader);
136+
}
137+
138+
return ChoiceList::loader($this, $loader, $vary);
132139
}
133140

134141
return null;
@@ -208,6 +215,7 @@ public function configureOptions(OptionsResolver $resolver): void
208215
'choice_value' => $choiceValue,
209216
'id_reader' => null, // internal
210217
'choice_translation_domain' => false,
218+
'extra_lazy' => false,
211219
]);
212220

213221
$resolver->setRequired(['class']);
@@ -217,6 +225,7 @@ public function configureOptions(OptionsResolver $resolver): void
217225
$resolver->setNormalizer('id_reader', $idReaderNormalizer);
218226

219227
$resolver->setAllowedTypes('em', ['null', 'string', ObjectManager::class]);
228+
$resolver->setAllowedTypes('extra_lazy', 'bool');
220229
}
221230

222231
/**

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,4 +1771,108 @@ public function testWithSameLoaderAndDifferentChoiceValueCallbacks()
17711771
$this->assertSame('Foo', $view['entity_two']->vars['choices']['Foo']->value);
17721772
$this->assertSame('Bar', $view['entity_two']->vars['choices']['Bar']->value);
17731773
}
1774+
1775+
public function testEmptyChoicesWhenLazy()
1776+
{
1777+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1778+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1779+
$this->persist([$entity1, $entity2]);
1780+
1781+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE)
1782+
->add('entity_one', self::TESTED_TYPE, [
1783+
'em' => 'default',
1784+
'class' => self::SINGLE_IDENT_CLASS,
1785+
'extra_lazy' => true,
1786+
])
1787+
->createView()
1788+
;
1789+
1790+
$this->assertCount(0, $view['entity_one']->vars['choices']);
1791+
}
1792+
1793+
public function testLoadedChoicesWhenLazyAndBoundData()
1794+
{
1795+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1796+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1797+
$this->persist([$entity1, $entity2]);
1798+
1799+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE, ['entity_one' => $entity1])
1800+
->add('entity_one', self::TESTED_TYPE, [
1801+
'em' => 'default',
1802+
'class' => self::SINGLE_IDENT_CLASS,
1803+
'extra_lazy' => true,
1804+
])
1805+
->createView()
1806+
;
1807+
1808+
$this->assertCount(1, $view['entity_one']->vars['choices']);
1809+
$this->assertSame('1', $view['entity_one']->vars['choices'][1]->value);
1810+
}
1811+
1812+
public function testLoadedChoicesWhenLazyAndSubmittedData()
1813+
{
1814+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1815+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1816+
$this->persist([$entity1, $entity2]);
1817+
1818+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE)
1819+
->add('entity_one', self::TESTED_TYPE, [
1820+
'em' => 'default',
1821+
'class' => self::SINGLE_IDENT_CLASS,
1822+
'extra_lazy' => true,
1823+
])
1824+
->submit(['entity_one' => '2'])
1825+
->createView()
1826+
;
1827+
1828+
$this->assertCount(1, $view['entity_one']->vars['choices']);
1829+
$this->assertSame('2', $view['entity_one']->vars['choices'][2]->value);
1830+
}
1831+
1832+
public function testEmptyChoicesWhenLazyAndEmptyDataIsSubmitted()
1833+
{
1834+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1835+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1836+
$this->persist([$entity1, $entity2]);
1837+
1838+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE, ['entity_one' => $entity1])
1839+
->add('entity_one', self::TESTED_TYPE, [
1840+
'em' => 'default',
1841+
'class' => self::SINGLE_IDENT_CLASS,
1842+
'extra_lazy' => true,
1843+
])
1844+
->submit([])
1845+
->createView()
1846+
;
1847+
1848+
$this->assertCount(0, $view['entity_one']->vars['choices']);
1849+
}
1850+
1851+
public function testErrorOnSubmitInvalidValuesWhenLazyAndCustomQueryBuilder()
1852+
{
1853+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1854+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1855+
$this->persist([$entity1, $entity2]);
1856+
$qb = $this->em
1857+
->createQueryBuilder()
1858+
->select('e')
1859+
->from(self::SINGLE_IDENT_CLASS, 'e')
1860+
->where('e.id = 2')
1861+
;
1862+
1863+
$form = $this->factory->create(FormTypeTest::TESTED_TYPE, ['entity_one' => $entity2])
1864+
->add('entity_one', self::TESTED_TYPE, [
1865+
'em' => 'default',
1866+
'class' => self::SINGLE_IDENT_CLASS,
1867+
'query_builder' => $qb,
1868+
'extra_lazy' => true,
1869+
])
1870+
->submit(['entity_one' => '1'])
1871+
;
1872+
$view = $form->createView();
1873+
1874+
$this->assertCount(0, $view['entity_one']->vars['choices']);
1875+
$this->assertCount(1, $errors = $form->getErrors(true));
1876+
$this->assertSame('The selected choice is invalid.', $errors->current()->getMessage());
1877+
}
17741878
}

src/Symfony/Bridge/Doctrine/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"symfony/dependency-injection": "^6.4|^7.0",
3030
"symfony/doctrine-messenger": "^6.4|^7.0",
3131
"symfony/expression-language": "^6.4|^7.0",
32-
"symfony/form": "^6.4|^7.0",
32+
"symfony/form": "^7.1",
3333
"symfony/http-kernel": "^6.4|^7.0",
3434
"symfony/lock": "^6.4|^7.0",
3535
"symfony/messenger": "^6.4|^7.0",
@@ -53,7 +53,7 @@
5353
"doctrine/orm": "<2.15",
5454
"symfony/cache": "<6.4",
5555
"symfony/dependency-injection": "<6.4",
56-
"symfony/form": "<6.4",
56+
"symfony/form": "<7.1",
5757
"symfony/http-foundation": "<6.4",
5858
"symfony/http-kernel": "<6.4",
5959
"symfony/lock": "<6.4",

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+
7.1
5+
---
6+
7+
* Add `ExtraLazyChoiceLoader` for loading choices on demand
8+
49
7.0
510
---
611

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Loader;
13+
14+
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
15+
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
16+
17+
/**
18+
* A choice loader that loads its choices and values lazily, only when necessary.
19+
*
20+
* @author Yonel Ceruto <yonelceruto@gmail.com>
21+
*/
22+
class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
23+
{
24+
private ?ChoiceListInterface $choiceList = null;
25+
26+
public function __construct(
27+
private readonly ChoiceLoaderInterface $loader,
28+
) {
29+
}
30+
31+
public function loadChoiceList(callable $value = null): ChoiceListInterface
32+
{
33+
return $this->choiceList ??= new ArrayChoiceList([], $value);
34+
}
35+
36+
public function loadChoicesForValues(array $values, callable $value = null): array
37+
{
38+
$choices = $this->loader->loadChoicesForValues($values, $value);
39+
$this->choiceList = new ArrayChoiceList($choices, $value);
40+
41+
return $choices;
42+
}
43+
44+
public function loadValuesForChoices(array $choices, callable $value = null): array
45+
{
46+
$values = $this->loader->loadValuesForChoices($choices, $value);
47+
48+
if ($this->choiceList?->getValuesForChoices($choices) !== $values) {
49+
$this->loadChoicesForValues($values, $value);
50+
}
51+
52+
return $values;
53+
}
54+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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\Tests\ChoiceList\Loader;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Form\ChoiceList\Loader\ExtraLazyChoiceLoader;
16+
use Symfony\Component\Form\Tests\Fixtures\ArrayChoiceLoader;
17+
18+
class ExtraLazyChoiceLoaderTest extends TestCase
19+
{
20+
private ExtraLazyChoiceLoader $loader;
21+
22+
protected function setUp(): void
23+
{
24+
$this->loader = new ExtraLazyChoiceLoader(new ArrayChoiceLoader(['A', 'B', 'C']));
25+
}
26+
27+
public function testInitialEmptyChoiceListLoading()
28+
{
29+
$this->assertSame([], $this->loader->loadChoiceList()->getChoices());
30+
}
31+
32+
public function testOnDemandChoiceListAfterLoadingValuesForChoices()
33+
{
34+
$this->loader->loadValuesForChoices(['A']);
35+
$this->assertSame(['A' => 'A'], $this->loader->loadChoiceList()->getChoices());
36+
}
37+
38+
public function testOnDemandChoiceListAfterLoadingChoicesForValues()
39+
{
40+
$this->loader->loadChoicesForValues(['B']);
41+
$this->assertSame(['B' => 'B'], $this->loader->loadChoiceList()->getChoices());
42+
}
43+
44+
public function testOnDemandChoiceList()
45+
{
46+
$this->loader->loadValuesForChoices(['A']);
47+
$this->loader->loadChoicesForValues(['B']);
48+
$this->assertSame(['B' => 'B'], $this->loader->loadChoiceList()->getChoices());
49+
}
50+
}

0 commit comments

Comments
 (0)
0