Skip to content

[Autocomplete] New autocomplete type #1158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Autocomplete/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 2.13.0

- Add new BaseEntityAutocompleteType

## 2.9.0

- Add support for symfony/asset-mapper
Expand Down
12 changes: 8 additions & 4 deletions src/Autocomplete/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Or, create the field by hand::

use Symfony\Bundle\SecurityBundle\Security;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;

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

public function getParent(): string
{
return ParentEntityAutocompleteType::class;
return BaseEntityAutocompleteType::class;
}
}

.. versionadded:: 2.13

``BaseEntityAutocompleteType`` is a new replacement for ``ParentEntityAutocompleteType``.

There are 3 important things:

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

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

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

``searchable_fields`` (default: ``null``)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Symfony\UX\Autocomplete\Doctrine\EntitySearchUtil;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\AutocompleteChoiceTypeExtension;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
use Symfony\UX\Autocomplete\Form\WrappedEntityTypeAutocompleter;
use Symfony\UX\Autocomplete\Maker\MakeAutocompleteField;
Expand Down Expand Up @@ -130,8 +131,16 @@ private function registerBasicServices(ContainerBuilder $container): void

private function registerFormServices(ContainerBuilder $container): void
{
$container
->register('ux.autocomplete.base_entity_type', BaseEntityAutocompleteType::class)
->setArguments([
new Reference('router'),
])
->addTag('form.type');

$container
->register('ux.autocomplete.entity_type', ParentEntityAutocompleteType::class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we deprecate this service? Or does that trigger a deprecation SIMPLY because it's a valid form type (even if it's unused)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deprecated

->setDeprecated('symfony/ux-autocomplete', '2.13', 'The "%service_id%" form type is deprecated since 2.13. Use "ux.autocomplete.base_entity_type" instead.')
->setArguments([
new Reference('router'),
])
Expand Down
2 changes: 2 additions & 0 deletions src/Autocomplete/src/Form/AutocompleteChoiceTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public function finishView(FormView $view, FormInterface $form, array $options):
$values = [];
if ($options['autocomplete_url']) {
$values['url'] = $options['autocomplete_url'];
} elseif ($form->getConfig()->hasAttribute('autocomplete_url')) {
$values['url'] = $form->getConfig()->getAttribute('autocomplete_url');
}

if ($options['options_as_html']) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
* Helps transform ParentEntityAutocompleteType into a EntityType that will not load all options.
*
* @internal
*
* @deprecated since 2.13
*/
final class AutocompleteEntityTypeSubscriber implements EventSubscriberInterface
{
Expand Down
104 changes: 104 additions & 0 deletions src/Autocomplete/src/Form/BaseEntityAutocompleteType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Autocomplete\Form;

use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\UX\Autocomplete\Form\ChoiceList\Loader\ExtraLazyChoiceLoader;

/**
* All form types that want to expose autocomplete functionality should use this for its getParent().
*/
final class BaseEntityAutocompleteType extends AbstractType
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
) {
}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setAttribute('autocomplete_url', $this->getAutocompleteUrl($builder, $options));
}

public function configureOptions(OptionsResolver $resolver): void
{
$choiceLoader = static function (Options $options, $loader) {
return new ExtraLazyChoiceLoader($loader);
};

$resolver->setDefaults([
'autocomplete' => true,
'choice_loader' => $choiceLoader,
// set to the fields to search on or null to search on all fields
'searchable_fields' => null,
// override the search logic - set to a callable:
// function(QueryBuilder $qb, string $query, EntityRepository $repository) {
// $qb->andWhere('entity.name LIKE :filter OR entity.description LIKE :filter')
// ->setParameter('filter', '%'.$query.'%');
// }
'filter_query' => null,
// set to the string role that's required to view the autocomplete results
// or a callable: function(Symfony\Component\Security\Core\Security $security): bool
'security' => false,
// set the max results number that a query on automatic endpoint return.
'max_results' => 10,
]);

$resolver->setAllowedTypes('security', ['boolean', 'string', 'callable']);
$resolver->setAllowedTypes('max_results', ['int', 'null']);
$resolver->setAllowedTypes('filter_query', ['callable', 'null']);
$resolver->setNormalizer('searchable_fields', function (Options $options, ?array $searchableFields) {
if (null !== $searchableFields && null !== $options['filter_query']) {
throw new RuntimeException('Both the searchable_fields and filter_query options cannot be set.');
}

return $searchableFields;
});
}

public function getParent(): string
{
return EntityType::class;
}

public function getBlockPrefix(): string
{
return 'ux_entity_autocomplete';
}

/**
* Uses the provided URL, or auto-generate from the provided alias.
*/
private function getAutocompleteUrl(FormBuilderInterface $builder, array $options): string
{
if ($options['autocomplete_url']) {
return $options['autocomplete_url'];
}

$formType = $builder->getType()->getInnerType();
$attribute = AsEntityAutocompleteField::getInstance($formType::class);

if (!$attribute) {
throw new \LogicException(sprintf('You must either provide your own autocomplete_url, or add #[AsEntityAutocompleteField] attribute to %s.', $formType::class));
}

return $this->urlGenerator->generate($attribute->getRoute(), [
'alias' => $attribute->getAlias() ?: AsEntityAutocompleteField::shortName($formType::class),
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Autocomplete\Form\ChoiceList\Loader;

use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;

/**
* Loads choices on demand only.
*/
class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
{
private ?ChoiceListInterface $choiceList = null;
private array $choices = [];
private bool $cached = false;

public function __construct(
private readonly ChoiceLoaderInterface $decorated,
) {
}

public function loadChoiceList(callable $value = null): ChoiceListInterface
{
if (null !== $this->choiceList && $this->cached) {
return $this->choiceList;
}

$this->cached = true;

return $this->choiceList = new ArrayChoiceList($this->choices, $value);
}

public function loadChoicesForValues(array $values, callable $value = null): array
{
if ($this->choices !== $choices = $this->decorated->loadChoicesForValues($values, $value)) {
$this->cached = false;
}

return $this->choices = $choices;
}

public function loadValuesForChoices(array $choices, callable $value = null): array
{
$values = $this->decorated->loadValuesForChoices($choices, $value);

if ([] === $values || [''] === $values) {
$newChoices = [];
} else {
$newChoices = $choices;
}

if ($this->choices !== $newChoices) {
$this->choices = $newChoices;
$this->cached = false;
}

return $values;
}
}
2 changes: 2 additions & 0 deletions src/Autocomplete/src/Form/ParentEntityAutocompleteType.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

/**
* All form types that want to expose autocomplete functionality should use this for its getParent().
*
* @deprecated since 2.13, use "Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType" instead
*/
final class ParentEntityAutocompleteType extends AbstractType implements DataMapperInterface
{
Expand Down
4 changes: 3 additions & 1 deletion src/Autocomplete/src/Form/WrappedEntityTypeAutocompleter.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ public function getGroupBy(): mixed
private function getFormOption(string $name): mixed
{
$form = $this->getForm();
$formOptions = $form['autocomplete']->getConfig()->getOptions();
// Remove when dropping support for ParentEntityAutocompleteType
$form = $form->has('autocomplete') ? $form->get('autocomplete') : $form;
$formOptions = $form->getConfig()->getOptions();

return $formOptions[$name] ?? null;
}
Expand Down
12 changes: 9 additions & 3 deletions src/Autocomplete/templates/autocomplete_form_theme.html.twig
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
{# EasyAdminAutocomplete form type #}
{% block ux_entity_autocomplete_widget %}
{{ form_widget(form.autocomplete, { attr: form.autocomplete.vars.attr|merge({ required: required }) }) }}
{% if form.autocomplete is defined %}
{{ form_widget(form.autocomplete, { attr: form.autocomplete.vars.attr|merge({ required: required }) }) }}
{% else %}
{{ form_widget(form) }}
{% endif %}
{% endblock ux_entity_autocomplete_widget %}

{% block ux_entity_autocomplete_label %}
{% set id = form.autocomplete.vars.id %}
{% if form.autocomplete is defined %}
{% set id = form.autocomplete.vars.id %}
{% endif %}
{{ block('form_label') }}
{% endblock ux_entity_autocomplete_label %}
{% endblock ux_entity_autocomplete_label %}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Ingredient;

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

public function getParent(): string
{
return ParentEntityAutocompleteType::class;
return BaseEntityAutocompleteType::class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
namespace Symfony\UX\Autocomplete\Tests\Fixtures\Form;

use Doctrine\ORM\EntityRepository;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;

#[AsEntityAutocompleteField]
class CategoryAutocompleteType extends AbstractType
Expand Down Expand Up @@ -48,6 +48,6 @@ public function configureOptions(OptionsResolver $resolver)

public function getParent(): string
{
return ParentEntityAutocompleteType::class;
return BaseEntityAutocompleteType::class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@

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

use Doctrine\ORM\EntityRepository;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Category;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;

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

public function getParent(): string
{
return ParentEntityAutocompleteType::class;
return BaseEntityAutocompleteType::class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
use Symfony\UX\Autocomplete\Tests\Fixtures\Entity\Ingredient;

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

public function getParent(): string
{
return ParentEntityAutocompleteType::class;
return BaseEntityAutocompleteType::class;
}
}
Loading