x-one/autocomplete-bundle

There is no license information available for the latest version (v2.2.1) of this package.

Integrates Symfony UX Autocomplete with additional enhancements

Installs: 2 337

Dependents: 0

Suggesters: 0

Security: 0

Type:symfony-bundle

v2.2.1 2025-08-03 11:35 UTC

README

Paczka integrująca Symfony UX Autocomplete, oferująca pomocnicze klasy rozwijające funkcjonalność oraz ułatwiające definicję pól typu autocomplete.

Instalacja

Dodaj prywatne repozytorium do pliku composer.json:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "git@bitbucket.org:majchw/autocomplete-bundle.git"
        }
    ]
}

Uwaga: wymagana będzie autoryzacja poprzez klucz SSH — instrukcja.

Następnie, zainstaluj paczkę:

composer require x-one/autocomplete-bundle

Symfony Flex powinien automatycznie dodać nowe wpisy do plików config/bundles.php oraz assets/controllers.json:

return [
    // ...
    XOne\Bundle\AutocompleteBundle\XOneAutocompleteBundle::class => ['all' => true],
];
{
    "controllers": {
        "@x-one/autocomplete-bundle": {
            "autocomplete": {
                "enabled": true,
                "fetch": "eager"
            }
        }
    }
}

Finalnie, przebuduj assety:

yarn install --force
yarn watch

Różnice w stosunku do Symfony UX Autocomplete

Ogólna definicja pól oraz możliwości konfiguracji znajdują się w oficjalnej dokumentacji Symfony UX Autocomplete.

Definicja pola autocomplete

Klasy formularzy definiujące pola typu autocomplete powinny rozszerzać klasę AbstractEntityAutocompleteType:

// src/Form/Type/ProductAutocompleteFormType.php

use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
use XOne\Bundle\AutocompleteBundle\Form\Type\AbstractEntityAutocompleteType;

#[AsEntityAutocompleteField]
class ProductAutocompleteFormType extends AbstractEntityAutocompleteType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'class' => Product::class,
            'choice_label' => 'name',
        ]);
    }
}

Dzięki temu nie trzeba podawać getParent(), które w rozszerzanej klasie zwraca AutocompleteEntityType zamiast ParentEntityAutocompleteType przedstawianego w oficjalnej dokumentacji.

Obsługa filtracji w repozytoriach

Repozytoria mogą teraz implementować interfejs AutocompleteRepositoryInterface, dzięki czemu będą automatyczne wykorzystywane w procesie filtracji. Przykładowo:

// src/Repository/ProductRepository.php

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use XOne\Bundle\AutocompleteBundle\Repository\AutocompleteRepositoryInterface;

class ProductRepository extends ServiceEntityRepository implements AutocompleteRepositoryInterface
{
    public function addAutocompleteCriteria(QueryBuilder $queryBuilder, array $parameters): void
    {
        $rootAlias = current($queryBuilder->getRootAliases());

        if ($query = $parameters['query']) {
            $queryBuilder
                ->andWhere($queryBuilder->expr()->like("$rootAlias.name", ':query'))
                ->setParameter('query', "%$query%");
        }
    }
}

Tablica $parameters zawiera parametry query z URL. Wyszukiwana wartość zawsze dostępna jest pod kluczem query.

Uwaga: zwrócenie QueryBuilder w opcji filter_query pola autocomplete pomija repozytorium!

Przekazywanie parametrów

Największym problemem Symfony UX Autocomplete jest brak możliwości przekazania dodatkowych parametrów do podzapytania — ponieważ opcje, jakie przekażemy do formularza, są utracone w momencie wysłania żądania AJAX dla autocomplete.

W celu rozwiązania tego problemu bundle wprowadza nową opcję autocomplete_parameters:

// src/Form/Type/CategoryFormType.php

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class CategoryFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        /** @var null|Category $category */
        $category = $options['data'];

        $builder
            ->add('products', ProductAutocompleteFormType::class, [
                'autocomplete_parameters' => [
                    'category_id' => $data?->getId(),
                ],
            ])
        ;
    }
}

Parametry te zostają przekazywane w URL zapytań AJAX. W powyższym przykładzie (zakładając, że przekazane ID kategorii to "1") będzie to:

/autocomplete/product?query=&product_id=1

Dzięki temu parametr ten można wyciągnąć w repozytorium:

// src/Repository/ProductRepository.php

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use XOne\Bundle\AutocompleteBundle\Repository\AutocompleteRepositoryInterface;

class ProductRepository extends ServiceEntityRepository implements AutocompleteRepositoryInterface
{
    public function addAutocompleteCriteria(QueryBuilder $queryBuilder, array $parameters): void
    {
        $rootAlias = current($queryBuilder->getRootAliases());

        if (isset($categoryId = $parameters['category_id'] ?? null)) {
            $queryBuilder
                ->andWhere($queryBuilder->expr()->eq("$rootAlias.category", ':category'))
                ->setParameter('category', $categoryId);
        }
    }
}

Parametry o wartościach z innych pól formularza

W przypadku, gdy wartość parametr ma być pobrany z innego pola formularza, do opcji autocomplete_parameters można przekazać konkretne pole formularza.

Poniższy przykład zakłada, że mamy formularz dla produktu, w którym możemy wybrać jego klasę oraz grupę. Grupa może być wybrana tylko spośród tych, które należą do wybranej klasy.

// src/Form/Type/ProductFormType.php

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class ProductFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('productClass', ProductClassAutocompleteFormType::class)
            ->add('productGroup', ProductGroupAutocompleteFormType::class, [
                'autocomplete_parameters' => [
                    'product_class_id' => $builder->get('productClass'),
                ],
            ])
        ;
    }
}

Jeśli nie ma opcji na pobranie pola formularza (bo powstaje on później w procesie budowania), można przekazać instancję FormReference, w której podajemy jedynie nazwę pola:

// src/Form/Type/ProductFormType.php

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use XOne\Bundle\AutocompleteBundle\Form\FormReference;

class ProductFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('productClass', ProductClassAutocompleteFormType::class)
            ->add('productGroup', ProductGroupAutocompleteFormType::class, [
                'autocomplete_parameters' => [
                    'product_class_id' => new FormReference('productClass'),
                ],
            ])
        ;
    }
}

Jeśli natomiast mamy pewność co do selektora CSS pola formularza, możemy przekazać go bezpośrednio:

// src/Form/Type/ProductFormType.php

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use XOne\Bundle\AutocompleteBundle\Form\FormReference;

class ProductFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('productClass', ProductClassAutocompleteFormType::class)
            ->add('productGroup', ProductGroupAutocompleteFormType::class, [
                'autocomplete_parameters' => [
                    'product_class_id' => '#product_form_productClass',
                ],
            ])
        ;
    }
}

W powyższych przypadkach, do żądania AJAX pobierającego grupy produktów możliwe do wyboru, zostanie doklejony parametr product_class_id z wartością wybraną w polu klasy produktu.

Przetwarzanie parametrów

Jeśli zaistnieje potrzeba, aby przetworzyć tablicę parametrów przed przekazaniem jej do repozytorium, w klasie typu pola autocomplete można zaimplementować interfejs AutocompleteParametersTransformerInterface:

// src/Form/Type/ProductAutocompleteFormType.php

use Symfony\Component\Security\Core\Security;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;
use XOne\Bundle\AutocompleteBundle\Form\AutocompleteParametersTransformerInterface;
use XOne\Bundle\AutocompleteBundle\Form\Type\AbstractEntityAutocompleteType;

#[AsEntityAutocompleteField]
class ProductAutocompleteFormType extends AbstractEntityAutocompleteType implements AutocompleteParametersTransformerInterface
{
    public function __construct(
        private Security $security,
    ) {
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'class' => Product::class,
            'choice_label' => 'name',
        ]);
    }

    public function transformAutocompleteParameters(array $parameters): array
    {
        // Add defaults...
        $parameters += [
	        'category_id' => null,
        ];

        // Cast to proper types if needed...
        if ($parameters['category_id']) {
            $parameters['category_id'] = (int) $parameters['category_id'];
        }

        // Add additional parameters...
        $parameters['user_id'] = $this->security->getUser()?->getId();

        return $parameters;
    }
}

Metoda transformAutocompleteParameters() otrzymuje tablicę parametrów (wraz z wartościami), które przyszły podczas żądania AJAX.

Parametry w polach niepowiązanych z encjami

W niektórych sytuacjach zachodzi potrzeba funkcjonalności autocomplete na polach ChoiceType z opcjami ładowanymi w sposób niestandardowy, np. customowym zapytaniem do bazy danych zwracającym tablicę klucz-wartość zamiast encji, lub z API.

W takim przypadku takowe pole konfigurujemy w następujący sposób:

$builder->add('client', ChoiceType::class, [
    // Włączamy mechanizm autocomplete — wtedy pole renderuje się jako TomSelect
    'autocomplete' => true,
    // Ustawiamy URL, na którym zwracamy listę opcji zawężonych przez parametry
    'autocomplete_url' => $this->urlGenerator->generate('panel_dummy_client_autocomplete'),
    // Ustawiamy parametry, na podstawie których chcemy zawężać listę opcji
    'autocomplete_parameters' => [
        'name' => '#id_pola_po_ktorym_chcemy_ograniczać_klientów_po_nazwie',
    ],
]);

Następnie tworzymy endpoint, który zwróci listę opcji w formacie JSON z uwzględnieniem parametrów:

#[Route('/dummy-clients/autocomplete', name: 'panel_dummy_client_autocomplete', methods: ['GET'])]
public function autocomplete(Request $request): JsonResponse
{
    $queryBuilder = $this->getEntityManager()->createQueryBuilder()
        ->select('DISTINCT c.name AS text', 'c.id AS value')
        ->from(DummyClient::class, 'c');

    if ($name = $request->get('name')) {
        $queryBuilder->andWhere('c.name LIKE :name')
            ->setParameter('name', '%'.$name.'%');
    }

    return new JsonResponse([
        'results' => $queryBuilder->getQuery()->getResult(),
    ]);
}

W tym przypadku zapytanie nie zwraca encji DummyClient, a tablicę z kluczami text oraz value. Z requestu jesteśmy w stanie pobrać przekazane parametry i na ich podstawie zawęzić wyniki — tak, jak na przykładzie robimy to z ograniczeniem klientów po nazwie.

Co ważne, endpoint wg. dokumentacji UX Autocomplete musi zwrócić dane w formacie:

{
    "results": [
        { "text": "Nagłówek 1", "value": "Wartość 1" },
        { "text": "Nagłówek 2", "value": "Wartość 2" }
    ]
}

Jeśli pole to może zawierać domyślną wartość (np. w akcji edycji), należy pamiętać o tym, aby zwrócić ją w początkowej tablicy choices, aby TomSelect mógł ją wyrenderować przed otwarciem selektora, np.:

$builder->add('clientId', ChoiceType::class, [
    // W tym przypadku wartością tego pola może być ID jakiegoś klienta. Dla przykładu załóżmy,
    // że wynosi ono "1". Taki identyfikator musi znaleźć się w tablicy "choices" jako "value".
    // Pobrać go możemy w dowolny sposób, np. za pomocą repozytorium.
    'choices' => [
        ['value' => 1, 'text' => 'Klient testowy 1'],
    ],
]);

Rozwój bundle

Upewnij się, że testy nie zwracają żadnego błędu, oraz kod jest poprawnie sformatowany:

composer run-script pre-commit-checks

Zbuduj pliki wynikowe:

bin/build-js

Wersjonowanie

Aby paczkę dało się zaktualizować przez composera, po zmergowaniu zmian do głównego brancha, należy utworzyć tag w formacie vX.Y.Z, np.

git tag -a v1.1.0 -m "Version v1.1.0"
git push origin --tags