8000 add LazyChoiceLoader and choice_lazy option · symfony/symfony@97ddd62 · GitHub
[go: up one dir, main page]

Skip to content

Commit 97ddd62

Browse files
committed
add LazyChoiceLoader and choice_lazy option
1 parent 6d6dd4a commit 97ddd62

File tree

8 files changed

+374
-16
lines changed

8 files changed

+374
-16
lines changed

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity;
3131
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity;
3232
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
33+
use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader;
3334
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
3435
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
3536
use Symfony\Component\Form\Exception\RuntimeException;
@@ -1758,4 +1759,128 @@ public function testWithSameLoaderAndDifferentChoiceValueCallbacks()
17581759
$this->assertSame('Foo', $view['entity_two']->vars['choices']['Foo']->value);
17591760
$this->assertSame('Bar', $view['entity_two']->vars['choices']['Bar']->value);
17601761
}
1762+
1763+
public function testEmptyChoicesWhenLazy()
1764+
{
1765+
if (!class_exists(LazyChoiceLoader::class)) {
1766+
$this->markTestSkipped('This test requires symfony/form 7.2 or superior.');
1767+
}
1768+
1769+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1770+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1771+
$this->persist([$entity1, $entity2]);
1772+
1773+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE)
1774+
->add('entity_one', self::TESTED_TYPE, [
1775+
'em' => 'default',
1776+
'class' => self::SINGLE_IDENT_CLASS,
1777+
'choice_lazy' => true,
1778+
])
1779+
->createView()
1780+
;
1781+
1782+
$this->assertCount(0, $view['entity_one']->vars['choices']);
1783+
}
1784+
1785+
public function testLoadedChoicesWhenLazyAndBoundData()
1786+
{
1787+
if (!class_exists(LazyChoiceLoader::class)) {
1788+
$this->markTestSkipped('This test requires symfony/form 7.2 or superior.');
1789+
}
1790+
1791+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1792+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1793+
$this->persist([$entity1, $entity2]);
1794+
1795+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE, ['entity_one' => $entity1])
1796+
->add('entity_one', self::TESTED_TYPE, [
1797+
'em' => 'default',
1798+
'class' => self::SINGLE_IDENT_CLASS,
1799+
'choice_lazy' => true,
1800+
])
1801+
->createView()
1802+
;
1803+
1804+
$this->assertCount(1, $view['entity_one']->vars['choices']);
1805+
$this->assertSame('1', $view['entity_one']->vars['choices'][1]->value);
1806+
}
1807+
1808+
public function testLoadedChoicesWhenLazyAndSubmittedData()
1809+
{
1810+
if (!class_exists(LazyChoiceLoader::class)) {
1811+
$this->markTestSkipped('This test requires symfony/form 7.2 or superior.');
1812+
}
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+
'choice_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+
if (!class_exists(LazyChoiceLoader::class)) {
1835+
$this->markTestSkipped('This test requires symfony/form 7.2 or superior.');
1836+
}
1837+
1838+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1839+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1840+
$this->persist([$entity1, $entity2]);
1841+
1842+
$view = $this->factory->create(FormTypeTest::TESTED_TYPE, ['entity_one' => $entity1])
1843+
->add('entity_one', self::TESTED_TYPE, [
1844+
'em' => 'default',
1845+
'class' => self::SINGLE_IDENT_CLASS,
1846+
'choice_lazy' => true,
1847+
])
1848+
->submit([])
1849+
->createView()
1850+
;
1851+
1852+
$this->assertCount(0, $view['entity_one']->vars['choices']);
1853+
}
1854+
1855+
public function testErrorOnSubmitInvalidValuesWhenLazyAndCustomQueryBuilder()
1856+
{
1857+
if (!class_exists(LazyChoiceLoader::class)) {
1858+
$this->markTestSkipped('This test requires symfony/form 7.2 or superior.');
1859+
}
1860+
1861+
$entity1 = new SingleIntIdEntity(1, 'Foo');
1862+
$entity2 = new SingleIntIdEntity(2, 'Bar');
1863+
$this->persist([$entity1, $entity2]);
1864+
$qb = $this->em
1865+
->createQueryBuilder()
1866+
->select('e')
1867+
->from(self::SINGLE_IDENT_CLASS, 'e')
1868+
->where('e.id = 2')
1869+
;
1870+
1871+
$form = $this->factory->create(FormTypeTest::TESTED_TYPE, ['entity_one' => $entity2])
1872+
->add('entity_one', self::TESTED_TYPE, [
1873+
'em' => 'default',
1874+
'class' => self::SINGLE_IDENT_CLASS,
1875+
'query_builder' => $qb,
1876+
'choice_lazy' => true,
1877+
])
1878+
->submit(['entity_one' => '1'])
1879+
;
1880+
$view = $form->createView();
1881+
1882+
$this->assertCount(0, $view['entity_one']->vars['choices']);
1883+
$this->assertCount(1, $errors = $form->getErrors(true));
1884+
$this->assertSame('The selected choice is invalid.', $errors->current()->getMessage());
1885+
}
17611886
}

src/Symfony/Component/Form/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate the `VersionAwareTest` trait, use feature detection instead
8+
* Add `LazyChoiceLoader` and `choice_lazy` option in `ChoiceType` for loading and rendering choices on demand
89

910
7.1
1011
---
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 LazyChoiceLoader 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+
}

src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
2828
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
2929
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
30+
use Symfony\Component\Form\ChoiceList\Loader\LazyChoiceLoader;
3031
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
3132
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
3233
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
3334
use Symfony\Component\Form\Event\PreSubmitEvent;
35+
use Symfony\Component\Form\Exception\LogicException;
3436
use Symfony\Component\Form\Exception\TransformationFailedException;
3537
use Symfony\Component\Form\Extension\Core\DataMapper\CheckboxListMapper;
3638
use Symfony\Component\Form\Extension\Core\DataMapper\RadioListMapper;
@@ -333,11 +335,24 @@ public function configureOptions(OptionsResolver $resolver): void
333335
return $choiceTranslationDomain;
334336
};
335337

338+
$choiceLoaderNormalizer = static function (Options $options, ?ChoiceLoaderInterface $choiceLoader) {
339+
if (!$options['choice_lazy']) {
340+
return $choiceLoader;
341+
}
342+
343+
if (null === $choiceLoader) {
344+
throw new LogicException('The "choice_lazy" option can only be used if the "choice_loader" option is set.');
345+
}
346+
347+
return new LazyChoiceLoader($choiceLoader);
348+
};
349+
336350
$resolver->setDefaults([
337351
'multiple' => false,
338352
'expanded' => false,
339353
'choices' => [],
340354
'choice_filter' => null,
355+
'choice_lazy' => false,
341356
'choice_loader' => null,
342357
'choice_label' => null,
343358
'choice_name' => null,
@@ -365,9 +380,11 @@ public function configureOptions(OptionsResolver $resolver): void
365380

366381
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
367382
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
383+
$resolver->setNormalizer('choice_loader', $choiceLoaderNormalizer);
368384

369385
$resolver->setAllowedTypes('choices', ['null', 'array', \Traversable::class]);
370386
$resolver->setAllowedTypes('choice_translation_domain', ['null', 'bool', 'string']);
387+
$resolver->setAllowedTypes('choice_lazy', 'bool');
371388
$resolver->setAllowedTypes('choice_loader', ['null', ChoiceLoaderInterface::class, ChoiceLoader::class]);
372389
$resolver->setAllowedTypes('choice_filter', ['null', 'callable', 'string', PropertyPath::class, ChoiceFilter::class]);
373390
$resolver->setAllowedTypes('choice_label', ['null', 'bool', 'callable', 'string', PropertyPath::class, ChoiceLabel::class]);
@@ -381,6 +398,8 @@ public function configureOptions(OptionsResolver $resolver): void
381398
$resolver->setAllowedTypes('separator_html', ['bool']);
382399
$resolver->setAllowedTypes('duplicate_preferred_choices', 'bool');
383400
$resolver->setAllowedTypes('group_by', ['null', 'callable', 'string', PropertyPath::class, GroupBy::class]);
401+
402+
$resolver->setInfo('choice_lazy', 'Load choices on demand. When set to true, only the selected choices are loaded and rendered.');
384403
}
385404

386405
public function getBlockPrefix(): string
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\LazyChoiceLoader;
16+
use Symfony\Component\Form\Tests\Fixtures\ArrayChoiceLoader;
17+
18+
class LazyChoiceLoaderTest extends TestCase
19+
{
20+
private LazyChoiceLoader $loader;
21+
22+
protected function setUp(): void
23+
{
24+
$this->loader = new LazyChoiceLoader(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