Skip to content

Navigation Menu

Sign in
Appearance settings

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 d73b5ee

Browse filesBrowse files
committed
add LazyChoiceLoader and choice_lazy option
1 parent c3bb47a commit d73b5ee
Copy full SHA for d73b5ee

File tree

8 files changed

+374
-16
lines changed
Filter options

8 files changed

+374
-16
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php
+125Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Deprecate the `VersionAwareTest` trait, use feature detection instead
88
* Add support for the `calendar` option in `DateType`
9+
* Add `LazyChoiceLoader` and `choice_lazy` option in `ChoiceType` for loading and rendering choices on demand
910

1011
7.1
1112
---
+54Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php
+19Lines 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
+50Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.