Objectifs

Nous allons découvrir comment mettre en place super simplement une autocompletion avec Symfony UX AutoComplete.

Histoire de ce projetter dans une situation réelle, nous testerons dans un premier temps avec une liste de 10 000 choix, puis de 100 000 choix !

Mise en place d’un projet de démo

  • Création d’un nouveau projet Symfony
symfony new SfAutocomplete --webapp
cd SfAutocomplete
docker compose up -d
symfony serve -d
  • Ajoutons Bootstrap dans notre fichier base.html.twig
<!doctype html>
<html lang="fr">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
		<title>Bootstrap demo</title>
		{% block stylesheets %}
			{{ encore_entry_link_tags('app') }}
		{% endblock %}
		<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
		{% block javascripts %}
			{{ encore_entry_script_tags('app') }}
		{% endblock %}
	</head>
	<body>
        <div class="container">
            {% block body %}{% endblock %}
        </div>
		<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
	</body>
</html>
  • Créons une première entité Choix
symfony console make:entity choix  

 created: src/Entity/Choix.php
 created: src/Repository/ChoixRepository.php

 New property name (press <return> to stop adding fields):
 > nom

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Choix.php
  • Créons une seconde entité Simple
symfony console make:entity Simple

 created: src/Entity/Simple.php
 created: src/Repository/SimpleRepository.php

 New property name (press <return> to stop adding fields):
 > nom

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Simple.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > choix

 Field type (enter ? to see all types) [string]:
 > relation

 What class should this entity be related to?:
 > Choix

 Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
 > ManyToOne

 Is the Simple.choix property allowed to be null (nullable)? (yes/no) [yes]:
 > no

 Do you want to add a new property to Choix so that you can access/update Simple objects from it - e.g. $choix->getSimples()? (yes/no) [yes]:
 > yes

 A new property will also be added to the Choix class so that you can access the related Simple objects from it.

 New field name inside Choix [simples]:
 > simples

 Do you want to automatically delete orphaned App\Entity\Simple objects (orphanRemoval)? (yes/no) [no]:
 > no

 updated: src/Entity/Simple.php
 updated: src/Entity/Choix.php
           
  Success!
  • Et enfin une troisième entité Multiple
symfony console make:entity Multiple

 created: src/Entity/Multiple.php
 created: src/Repository/MultipleRepository.php

 New property name (press <return> to stop adding fields):
 > nom

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Multiple.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > Choix

 Field type (enter ? to see all types) [string]:
 > relation

 What class should this entity be related to?:
 > Choix

 Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
 > ManyToMany

 Do you want to add a new property to Choix so that you can access/update Multiple objects from it - e.g. $choix->getMultiples()? (yes/no) [yes]:
 > yes

 A new property will also be added to the Choix class so that you can access the related Multiple objects from it.

 New field name inside Choix [multiples]:
 > multiples

 updated: src/Entity/Multiple.php
 updated: src/Entity/Choix.php
   
  Success!
  • Générons les migrations et appliquons les
symfony console make:migration
symfony console d:m:m

Création “express” des CRUDs

  • Créons un CRUD pour l’entité Simple
symfony console make:crud           

 The class name of the entity to create CRUD (e.g. OrangeKangaroo):
 > Simple

 Choose a name for your controller class (e.g. SimpleController) [SimpleController]:
 > SimpleController

 Do you want to generate tests for the controller?. [Experimental] (yes/no) [no]:
 > no 

 created: src/Controller/SimpleController.php
 created: src/Form/SimpleType.php
 created: templates/simple/_delete_form.html.twig
 created: templates/simple/_form.html.twig
 created: templates/simple/edit.html.twig
 created: templates/simple/index.html.twig
 created: templates/simple/new.html.twig
 created: templates/simple/show.html.twig

           
  Success!
  • Créons un CRUD pour l’entité Multiple
symfony console make:crud

 The class name of the entity to create CRUD (e.g. OrangeGnome):
 > Multiple

 Choose a name for your controller class (e.g. MultipleController) [MultipleController]:
 > MultipleController

 Do you want to generate tests for the controller?. [Experimental] (yes/no) [no]:
 > no

 created: src/Controller/MultipleController.php
 created: src/Form/MultipleType.php
 created: templates/multiple/_delete_form.html.twig
 created: templates/multiple/_form.html.twig
 created: templates/multiple/edit.html.twig
 created: templates/multiple/index.html.twig
 created: templates/multiple/new.html.twig
 created: templates/multiple/show.html.twig

           
  Success!

Génération de fausse données

  • Intallons quelques outils
composer require --dev orm-fixtures
composer require --dev fakerphp/faker
  • Ecrivons les fixtures pour l’entité Choix
// src/DataFixtures/AppFixtures.php
<?php

namespace App\DataFixtures;

use App\Entity\Choix;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $faker = Factory::create('fr_FR');

        for ($i=0; $i < 10000; $i++) { 
            $choix = new Choix();
            $choix->setNom($faker->name);

            $manager->persist($choix);
        }

        $manager->flush();
    }
}

Adaptation de notre entité Choix

Pour que notre entité retourne une string, ajoutons lui la méthode suivante 🪄

public function __toString(): string
    {
        return $this->nom;
    }

Testons nos formulaires

Sans surprise, les formulaires fonctionne bien, mais pas simple de naviguer dans cette longue liste de choix !

Symfony UX Autocomplete : Let’s go!

  • Installons Webpack Encore et Symfony UI Autocomplete
composer require symfony/webpack-encore-bundle
composer require symfony/ux-autocomplete
npm install --force && npm run build
  • Mettons en place l’autocomplete (sans Ajax) pour le formulaire src/Form/SimpleType.php
<?php

namespace App\Form;

use App\Entity\Choix;
use App\Entity\Simple;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SimpleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('nom')
            ->add('choix', EntityType::class, [
                'class' => Choix::class,
                'choice_label' => 'nom',
                'placeholder' => 'Choisissez un nom dans la liste',
                'autocomplete' => true,
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Simple::class,
        ]);
    }
}
  • Mettons en place l’autocomplete (sans Ajax) pour le formulaire src/Form/MultipleType.php
<?php

namespace App\Form;

use App\Entity\Choix;
use App\Entity\Multiple;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MultipleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('nom')
            ->add('choix', EntityType::class, [
                'class' => Choix::class,
                'choice_label' => 'nom',
                'placeholder' => 'Choisissez un nom dans la liste',
                'autocomplete' => true,
                'multiple' => true,
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Multiple::class,
        ]);
    }
}
  • Et laissons la magie opérer

Créons nous un problème 🤯

  • Modifions nos fixtures src/DataFixtures/AppFixtures.php pour générer non pas 10 000 choix mais 100 000 choix !
class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $faker = Factory::create('fr_FR');

        for ($i=0; $i < 100000; $i++) { 
            $choix = new Choix();
            $choix->setNom($faker->name);

            $manager->persist($choix);
        }

        $manager->flush();
    }
}
  • Testons nos formulaires : BOOM… Fatal error !

Réglons le problème 😎

Pour le formulaire src/Form/SimpleType.php

  • Générons un autocomplete-field
php bin/console make:autocomplete-field

 The class name of the entity you want to autocomplete:
 > Choix

 Choose a name for your entity field class (e.g. ChoixAutocompleteField) [ChoixAutocompleteField]:
 > SimpleChoixAutocompleteField

 created: src/Form/SimpleChoixAutocompleteField.php

  Success!
  • Modifions le fichier `src/Form/SimpleChoixAutocompleteField.php'
<?php

namespace App\Form;

use App\Entity\Choix;
use App\Repository\ChoixRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;

#[AsEntityAutocompleteField]
class SimpleChoixAutocompleteField extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'class' => Choix::class,
            'placeholder' => 'Choissisez un nom dans la liste',
            'choice_label' => 'nom',

            'query_builder' => function(ChoixRepository $choixRepository) {
                return $choixRepository->createQueryBuilder('choix');
            },
            //'security' => 'ROLE_SOMETHING',
        ]);
    }

    public function getParent(): string
    {
        return ParentEntityAutocompleteType::class;
    }
}
  • Et adaptons notre formulaire src/Form/SimpleType.php
<?php

namespace App\Form;

use App\Entity\Choix;
use App\Entity\Simple;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SimpleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('nom')
            ->add('choix', SimpleChoixAutocompleteField::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Simple::class,
        ]);
    }
}

Pour le formulaire src/Form/MultipleType.php

  • Générons un autocomplete-field
php bin/console make:autocomplete-field


 The class name of the entity you want to autocomplete:
 > Choix

 Choose a name for your entity field class (e.g. ChoixAutocompleteField) [ChoixAutocompleteField]:
 > MultipleChoixAutocompleteField

 created: src/Form/MultipleChoixAutocompleteField.php

           
  Success!
  • Modifions le fichier `src/Form/MultipleChoixAutocompleteField.php'
<?php

namespace App\Form;

use App\Entity\Choix;
use App\Repository\ChoixRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\ParentEntityAutocompleteType;

#[AsEntityAutocompleteField]
class MultipleChoixAutocompleteField extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'class' => Choix::class,
            'placeholder' => 'Choissiez des noms dans la liste',
            'choice_label' => 'nom',
            'multiple' => true,
            'query_builder' => function(ChoixRepository $choixRepository) {
                return $choixRepository->createQueryBuilder('choix');
            },
            //'security' => 'ROLE_SOMETHING',
        ]);
    }

    public function getParent(): string
    {
        return ParentEntityAutocompleteType::class;
    }
}
  • Et adaptons notre formulaire src/Form/MultipleType.php
<?php

namespace App\Form;

use App\Entity\Multiple;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MultipleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('nom')
            ->add('choix', MultipleChoixAutocompleteField::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Multiple::class,
        ]);
    }
}

Que le spectacle commence 🚀

Allons constater le résultat ! Magie !

Conclusion

Nous venons de le voir, avec Symgony UX Autocomplete, la mise en place d’un autocomplete pour nos formulaires et d’une simplicité déconcertante !

Le code source de la démo est dispo sur GitHub.