Skip to content

Commit f52d7af

Browse files
committed
feature #1158 [Autocomplete] New autocomplete type (Yonel Ceruto, norkunas)
This PR was merged into the 2.x branch. Discussion ---------- [Autocomplete] New autocomplete type | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | #1129 | License | MIT Continuation of work from `@yceruto`. One thing that bothers me: in the `ParentEntityAutocompleteType` we have `getBlockPrefix(): { return 'ux_entity_autocomplete'; }`. If we'd add this to the new one, it'd break my custom form theme because I access `form.autocomplete.vars..`, should we set something new for block prefix? Commits ------- 9335587 Fix tests, add deprecations and update CHANGELOG fa774c6 Keeping BC 17672a6 Refactoring Autocomplete Form Type
2 parents 662afed + 9335587 commit f52d7af

15 files changed

+235
-36
lines changed

src/Autocomplete/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.13.0
4+
5+
- Add new BaseEntityAutocompleteType
6+
37
## 2.9.0
48

59
- Add support for symfony/asset-mapper

src/Autocomplete/doc/index.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Or, create the field by hand::
103103

104104
use Symfony\Bundle\SecurityBundle\Security;
105105
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
106-
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
106+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
107107

108108
#[AsEntityAutocompleteField]
109109
class FoodAutocompleteField extends AbstractType
@@ -128,15 +128,19 @@ Or, create the field by hand::
128128

129129
public function getParent(): string
130130
{
131-
return ParentEntityAutocompleteType::class;
131+
return BaseEntityAutocompleteType::class;
132132
}
133133
}
134134

135+
.. versionadded:: 2.13
136+
137+
``BaseEntityAutocompleteType`` is a new replacement for ``ParentEntityAutocompleteType``.
138+
135139
There are 3 important things:
136140

137141
#. The class needs the ``#[AsEntityAutocompleteField]`` attribute so that
138142
it's noticed by the autocomplete system.
139-
#. The ``getParent()`` method must return ``ParentEntityAutocompleteType``.
143+
#. The ``getParent()`` method must return ``BaseEntityAutocompleteType``.
140144
#. Inside ``configureOptions()``, you can configure your field using whatever
141145
normal ``EntityType`` options you need plus a few extra options (see `Form Options Reference`_).
142146

@@ -216,7 +220,7 @@ e.g. ``FoodAutocompleteField`` from above):
216220
is automatically translated using the ``AutocompleteBundle`` domain.
217221

218222
For the Ajax-powered autocomplete field classes (i.e. those whose
219-
``getParent()`` returns ``ParentEntityAutocompleteType``), in addition
223+
``getParent()`` returns ``BaseEntityAutocompleteType``), in addition
220224
to the options above, you can also pass:
221225

222226
``searchable_fields`` (default: ``null``)

src/Autocomplete/src/DependencyInjection/AutocompleteExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil;
2828
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
2929
use Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension;
30+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
3031
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
3132
use Symfony\UX\Autocomplete\Form\WrappedEntityTypeAutocompleter;
3233
use Symfony\UX\Autocomplete\Maker\MakeAutocompleteField;
@@ -130,8 +131,16 @@ private function registerBasicServices(ContainerBuilder $container): void
130131

131132
private function registerFormServices(ContainerBuilder $container): void
132133
{
134+
$container
135+
->register('ux.autocomplete.base_entity_type', BaseEntityAutocompleteType::class)
136+
->setArguments([
137+
new Reference('router'),
138+
])
139+
->addTag('form.type');
140+
133141
$container
134142
->register('ux.autocomplete.entity_type', ParentEntityAutocompleteType::class)
143+
->setDeprecated('symfony/ux-autocomplete', '2.13', 'The "%service_id%" form type is deprecated since 2.13. Use "ux.autocomplete.base_entity_type" instead.')
135144
->setArguments([
136145
new Reference('router'),
137146
])

src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public function finishView(FormView $view, FormInterface $form, array $options):
5555
$values = [];
5656
if ($options['autocomplete_url']) {
5757
$values['url'] = $options['autocomplete_url'];
58+
} elseif ($form->getConfig()->hasAttribute('autocomplete_url')) {
59+
$values['url'] = $form->getConfig()->getAttribute('autocomplete_url');
5860
}
5961

6062
if ($options['options_as_html']) {

src/Autocomplete/src/Form/AutocompleteEntityTypeSubscriber.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* Helps transform ParentEntityAutocompleteType into a EntityType that will not load all options.
2323
*
2424
* @internal
25+
*
26+
* @deprecated since 2.13
2527
*/
2628
final class AutocompleteEntityTypeSubscriber implements EventSubscriberInterface
2729
{
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\Autocomplete\Form;
13+
14+
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
15+
use Symfony\Component\Form\AbstractType;
16+
use Symfony\Component\Form\Exception\RuntimeException;
17+
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\OptionsResolver\Options;
19+
use Symfony\Component\OptionsResolver\OptionsResolver;
20+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
21+
use Symfony\UX\Autocomplete\Form\ChoiceList\Loader\ExtraLazyChoiceLoader;
22+
23+
/**
24+
* All form types that want to expose autocomplete functionality should use this for its getParent().
25+
*/
26+
final class BaseEntityAutocompleteType extends AbstractType
27+
{
28+
public function __construct(
29+
private UrlGeneratorInterface $urlGenerator,
30+
) {
31+
}
32+
33+
public function buildForm(FormBuilderInterface $builder, array $options): void
34+
{
35+
$builder->setAttribute('autocomplete_url', $this->getAutocompleteUrl($builder, $options));
36+
}
37+
38+
public function configureOptions(OptionsResolver $resolver): void
39+
{
40+
$choiceLoader = static function (Options $options, $loader) {
41+
return new ExtraLazyChoiceLoader($loader);
42+
};
43+
44+
$resolver->setDefaults([
45+
'autocomplete' => true,
46+
'choice_loader' => $choiceLoader,
47+
// set to the fields to search on or null to search on all fields
48+
'searchable_fields' => null,
49+
// override the search logic - set to a callable:
50+
// function(QueryBuilder $qb, string $query, EntityRepository $repository) {
51+
// $qb->andWhere('entity.name LIKE :filter OR entity.description LIKE :filter')
52+
// ->setParameter('filter', '%'.$query.'%');
53+
// }
54+
'filter_query' => null,
55+
// set to the string role that's required to view the autocomplete results
56+
// or a callable: function(Symfony\Component\Security\Core\Security $security): bool
57+
'security' => false,
58+
// set the max results number that a query on automatic endpoint return.
59+
'max_results' => 10,
60+
]);
61+
62+
$resolver->setAllowedTypes('security', ['boolean', 'string', 'callable']);
63+
$resolver->setAllowedTypes('max_results', ['int', 'null']);
64+
$resolver->setAllowedTypes('filter_query', ['callable', 'null']);
65+
$resolver->setNormalizer('searchable_fields', function (Options $options, ?array $searchableFields) {
66+
if (null !== $searchableFields && null !== $options['filter_query']) {
67+
throw new RuntimeException('Both the searchable_fields and filter_query options cannot be set.');
68+
}
69+
70+
return $searchableFields;
71+
});
72+
}
73+
74+
public function getParent(): string
75+
{
76+
return EntityType::class;
77+
}
78+
79+
public function getBlockPrefix(): string
80+
{
81+
return 'ux_entity_autocomplete';
82+
}
83+
84+
/**
85+
* Uses the provided URL, or auto-generate from the provided alias.
86+
*/
87+
private function getAutocompleteUrl(FormBuilderInterface $builder, array $options): string
88+
{
89+
if ($options['autocomplete_url']) {
90+
return $options['autocomplete_url'];
91+
}
92+
93+
$formType = $builder->getType()->getInnerType();
94+
$attribute = AsEntityAutocompleteField::getInstance($formType::class);
95+
96+
if (!$attribute) {
97+
throw new \LogicException(sprintf('You must either provide your own autocomplete_url, or add #[AsEntityAutocompleteField] attribute to %s.', $formType::class));
98+
}
99+
100+
return $this->urlGenerator->generate($attribute->getRoute(), [
101+
'alias' => $attribute->getAlias() ?: AsEntityAutocompleteField::shortName($formType::class),
102+
]);
103+
}
104+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\UX\Autocomplete\Form\ChoiceList\Loader;
13+
14+
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
15+
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
16+
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
17+
18+
/**
19+
* Loads choices on demand only.
20+
*/
21+
class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
22+
{
23+
private ?ChoiceListInterface $choiceList = null;
24+
private array $choices = [];
25+
private bool $cached = false;
26+
27+
public function __construct(
28+
private readonly ChoiceLoaderInterface $decorated,
29+
) {
30+
}
31+
32+
public function loadChoiceList(callable $value = null): ChoiceListInterface
33+
{
34+
if (null !== $this->choiceList && $this->cached) {
35+
return $this->choiceList;
36+
}
37+
38+
$this->cached = true;
39+
40+
return $this->choiceList = new ArrayChoiceList($this->choices, $value);
41+
}
42+
43+
public function loadChoicesForValues(array $values, callable $value = null): array
44+
{
45+
if ($this->choices !== $choices = $this->decorated->loadChoicesForValues($values, $value)) {
46+
$this->cached = false;
47+
}
48+
49+
return $this->choices = $choices;
50+
}
51+
52+
public function loadValuesForChoices(array $choices, callable $value = null): array
53+
{
54+
$values = $this->decorated->loadValuesForChoices($choices, $value);
55+
56+
if ([] === $values || [''] === $values) {
57+
$newChoices = [];
58+
} else {
59+
$newChoices = $choices;
60+
}
61+
62+
if ($this->choices !== $newChoices) {
63+
$this->choices = $newChoices;
64+
$this->cached = false;
65+
}
66+
67+
return $values;
68+
}
69+
}

src/Autocomplete/src/Form/ParentEntityAutocompleteType.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
/**
2525
* All form types that want to expose autocomplete functionality should use this for its getParent().
26+
*
27+
* @deprecated since 2.13, use "Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType" instead
2628
*/
2729
final class ParentEntityAutocompleteType extends AbstractType implements DataMapperInterface
2830
{

src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ public function getGroupBy(): mixed
120120
private function getFormOption(string $name): mixed
121121
{
122122
$form = $this->getForm();
123-
$formOptions = $form['autocomplete']->getConfig()->getOptions();
123+
// Remove when dropping support for ParentEntityAutocompleteType
124+
$form = $form->has('autocomplete') ? $form->get('autocomplete') : $form;
125+
$formOptions = $form->getConfig()->getOptions();
124126

125127
return $formOptions[$name] ?? null;
126128
}
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
{# EasyAdminAutocomplete form type #}
22
{% block ux_entity_autocomplete_widget %}
3-
{{ form_widget(form.autocomplete, { attr: form.autocomplete.vars.attr|merge({ required: required }) }) }}
3+
{% if form.autocomplete is defined %}
4+
{{ form_widget(form.autocomplete, { attr: form.autocomplete.vars.attr|merge({ required: required }) }) }}
5+
{% else %}
6+
{{ form_widget(form) }}
7+
{% endif %}
48
{% endblock ux_entity_autocomplete_widget %}
59

610
{% block ux_entity_autocomplete_label %}
7-
{% set id = form.autocomplete.vars.id %}
11+
{% if form.autocomplete is defined %}
12+
{% set id = form.autocomplete.vars.id %}
13+
{% endif %}
814
{{ block('form_label') }}
9-
{% endblock ux_entity_autocomplete_label %}
15+
{% endblock ux_entity_autocomplete_label %}

src/Autocomplete/tests/Fixtures/Form/AlternateRouteAutocompleteType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Symfony\Component\Form\AbstractType;
66
use Symfony\Component\OptionsResolver\OptionsResolver;
77
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
8-
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
8+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
99
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Ingredient;
1010

1111
#[AsEntityAutocompleteField(route: 'ux_autocomplete_alternate')]
@@ -24,6 +24,6 @@ public function configureOptions(OptionsResolver $resolver)
2424

2525
public function getParent(): string
2626
{
27-
return ParentEntityAutocompleteType::class;
27+
return BaseEntityAutocompleteType::class;
2828
}
2929
}

src/Autocomplete/tests/Fixtures/Form/CategoryAutocompleteType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
44

55
use Doctrine\ORM\EntityRepository;
6+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
67
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
78
use Symfony\Component\Form\AbstractType;
89
use Symfony\Component\HttpFoundation\RequestStack;
910
use Symfony\Component\OptionsResolver\OptionsResolver;
1011
use Symfony\Bundle\SecurityBundle\Security;
1112
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
12-
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
1313

1414
#[AsEntityAutocompleteField]
1515
class CategoryAutocompleteType extends AbstractType
@@ -48,6 +48,6 @@ public function configureOptions(OptionsResolver $resolver)
4848

4949
public function getParent(): string
5050
{
51-
return ParentEntityAutocompleteType::class;
51+
return BaseEntityAutocompleteType::class;
5252
}
5353
}

src/Autocomplete/tests/Fixtures/Form/CategoryNoChoiceLabelAutocompleteType.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22

33
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;
44

5-
use Doctrine\ORM\EntityRepository;
5+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
66
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
77
use Symfony\Component\Form\AbstractType;
8-
use Symfony\Component\HttpFoundation\RequestStack;
98
use Symfony\Component\OptionsResolver\OptionsResolver;
10-
use Symfony\Bundle\SecurityBundle\Security;
119
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
12-
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
1310

1411
#[AsEntityAutocompleteField]
1512
class CategoryNoChoiceLabelAutocompleteType extends AbstractType
@@ -24,6 +21,6 @@ public function configureOptions(OptionsResolver $resolver)
2421

2522
public function getParent(): string
2623
{
27-
return ParentEntityAutocompleteType::class;
24+
return BaseEntityAutocompleteType::class;
2825
}
2926
}

src/Autocomplete/tests/Fixtures/Form/IngredientAutocompleteType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Symfony\Component\Form\AbstractType;
66
use Symfony\Component\OptionsResolver\OptionsResolver;
77
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
8-
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
8+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
99
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Ingredient;
1010

1111
#[AsEntityAutocompleteField]
@@ -24,6 +24,6 @@ public function configureOptions(OptionsResolver $resolver)
2424

2525
public function getParent(): string
2626
{
27-
return ParentEntityAutocompleteType::class;
27+
return BaseEntityAutocompleteType::class;
2828
}
2929
}

0 commit comments

Comments
 (0)