Symfony9 min

EntityType en Symfony : maitriser la selection d'entites

Par Pierre-Arthur Demengel
SymfonyFormsEntityTypeDoctrine

EntityType est l'un des types de champ les plus utilises dans les formulaires Symfony - et l'un des plus mal compris. Selection d'une categorie, association d'un auteur, choix multiple de tags : ce guide couvre tout, du cas simple aux optimisations avancees pour les tables volumineuses.

EntityType : les bases

EntityType est un type de champ qui genere un select HTML peuple automatiquement depuis une table Doctrine. Il herite de ChoiceType et ajoute l'integration avec l'ORM.

Cas le plus simple

Prenons un formulaire de creation d'article qui doit permettre de selectionner une categorie :

<?php

namespace App\Form;

use App\Entity\Article;
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title')
            ->add('content')
            ->add('category', EntityType::class, [
                'class' => Category::class,
                'choice_label' => 'name',
                'placeholder' => 'Choisir une categorie',
            ]);
    }

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

L'option class est obligatoire - elle indique a Symfony quelle entite Doctrine charger. L'option choice_label definit quelle propriete afficher dans le select. Sans elle, Symfony appellera la methode __toString() de l'entite.

choice_label : controler l'affichage

Trois facons de definir le label affiche pour chaque option :

Propriete simple

->add('category', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name', // Appelle $category->getName()
])

Callback pour un label compose

->add('author', EntityType::class, [
    'class' => User::class,
    'choice_label' => function (User $user): string {
        return sprintf('%s %s (%s)', $user->getFirstName(), $user->getLastName(), $user->getEmail());
    },
])

Methode __toString sur l'entite

<?php

// src/Entity/Category.php
class Category
{
    // ...

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

// Dans le formulaire - pas besoin de choice_label
->add('category', EntityType::class, [
    'class' => Category::class,
    // __toString() sera appelee automatiquement
])

La callback est la methode la plus flexible. Elle permet de combiner plusieurs proprietes et d'ajouter du contexte sans polluer l'entite avec un __toString() qui pourrait ne pas convenir partout.

query_builder : filtrer les choix

Par defaut, EntityType charge toutes les entites de la table. L'option query_builder permet de filtrer et trier les resultats :

use Doctrine\ORM\QueryBuilder;

->add('category', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name',
    'query_builder' => function (CategoryRepository $repo): QueryBuilder {
        return $repo->createQueryBuilder('c')
            ->where('c.active = :active')
            ->setParameter('active', true)
            ->orderBy('c.name', 'ASC');
    },
])

C'est indispensable dans la plupart des cas reels. Vous ne voulez pas afficher les categories supprimees, les utilisateurs inactifs ou les produits en rupture. Le query_builder recoit le repository de l'entite et doit retourner un QueryBuilder (pas un resultat).

Filtrage dynamique base sur le contexte

Parfois, les choix dependent d'un parametre contextuel. Passez-le via les options du formulaire :

<?php

// Dans le FormType
class ProjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $company = $options['company'];

        $builder->add('manager', EntityType::class, [
            'class' => User::class,
            'choice_label' => 'fullName',
            'query_builder' => function (UserRepository $repo) use ($company): QueryBuilder {
                return $repo->createQueryBuilder('u')
                    ->where('u.company = :company')
                    ->andWhere('u.role = :role')
                    ->setParameter('company', $company)
                    ->setParameter('role', 'ROLE_MANAGER')
                    ->orderBy('u.lastName', 'ASC');
            },
        ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Project::class,
        ]);
        $resolver->setRequired('company');
        $resolver->setAllowedTypes('company', Company::class);
    }
}

// Dans le controller
$form = $this->createForm(ProjectType::class, $project, [
    'company' => $this->getUser()->getCompany(),
]);

Selection multiple

Pour les relations ManyToMany ou les selections multiples, ajoutez multiple => true :

->add('tags', EntityType::class, [
    'class' => Tag::class,
    'choice_label' => 'name',
    'multiple' => true,
    'expanded' => false, // false = select multiple, true = checkboxes
    'by_reference' => false, // Important pour ManyToMany !
    'query_builder' => function (TagRepository $repo): QueryBuilder {
        return $repo->createQueryBuilder('t')
            ->orderBy('t.name', 'ASC');
    },
    'attr' => [
        'class' => 'select2', // Pour un widget JS ameliore
    ],
])

L'option by_reference => false est cruciale pour les relations ManyToMany. Sans elle, Symfony modifie la collection directement sans appeler vos methodes addTag() et removeTag(), ce qui peut casser la synchronisation bidirectionnelle de Doctrine.

Mode expanded : checkboxes et radios

// Checkboxes (multiple + expanded)
->add('skills', EntityType::class, [
    'class' => Skill::class,
    'choice_label' => 'name',
    'multiple' => true,
    'expanded' => true, // Genere des checkboxes
    'by_reference' => false,
])

// Boutons radio (single + expanded)
->add('priority', EntityType::class, [
    'class' => Priority::class,
    'choice_label' => 'label',
    'multiple' => false,
    'expanded' => true, // Genere des boutons radio
])

group_by : organiser les options

L'option group_by genere des balises <optgroup> dans le HTML pour structurer visuellement les choix :

->add('city', EntityType::class, [
    'class' => City::class,
    'choice_label' => 'name',
    'group_by' => function (City $city): string {
        return $city->getCountry()->getName();
    },
    'query_builder' => function (CityRepository $repo): QueryBuilder {
        return $repo->createQueryBuilder('c')
            ->leftJoin('c.country', 'co')
            ->addSelect('co') // Evite le N+1
            ->orderBy('co.name', 'ASC')
            ->addOrderBy('c.name', 'ASC');
    },
])

Notez le leftJoin avec addSelect dans le query_builder. Sans cela, chaque appel a $city->getCountry() dans le group_by declencherait une requete SQL supplementaire - c'est le fameux probleme N+1. Pour aller plus loin sur les requetes Doctrine, consultez notre guide sur FindBy et FindAll.

choice_attr : attributs HTML par option

Ajoutez des attributs HTML personnalises sur chaque option pour enrichir l'interface :

->add('product', EntityType::class, [
    'class' => Product::class,
    'choice_label' => 'name',
    'choice_attr' => function (Product $product): array {
        return [
            'data-price' => $product->getPrice(),
            'data-stock' => $product->getStock(),
            'disabled' => $product->getStock() === 0,
        ];
    },
])

Ces attributs data-* sont accessibles en JavaScript pour creer des interactions dynamiques - par exemple, afficher le prix a cote du select quand l'utilisateur change de produit.

Performance : les pieges a eviter

Tables volumineuses

EntityType charge toutes les entites correspondantes en memoire. Pour une table de 100 lignes, aucun probleme. Pour 10 000+ lignes, c'est une catastrophe. Solutions :

  • query_builder avec LIMIT - restreignez les resultats aux plus pertinents
  • Autocompletion AJAX - utilisez Symfony UX Autocomplete pour charger les options a la demande
  • Option choices - passez directement une collection pre-chargee et paginee
// Pour les tables volumineuses : autocompletion
// composer require symfony/ux-autocomplete

use Symfony\Component\Form\AbstractType;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;

#[AsEntityAutocompleteField]
class CustomerAutocompleteType extends AbstractType
{
    // Configure l'autocompletion avec recherche AJAX
}

N+1 queries

Quand vous utilisez choice_label ou group_by avec des proprietes de relations, chaque option peut declencher une requete supplementaire. La solution : toujours joindre les relations dans le query_builder :

'query_builder' => function (ProductRepository $repo): QueryBuilder {
    return $repo->createQueryBuilder('p')
        ->leftJoin('p.category', 'c')
        ->addSelect('c')        // Pre-charge la relation
        ->leftJoin('p.brand', 'b')
        ->addSelect('b')        // Pre-charge aussi la marque
        ->orderBy('p.name', 'ASC');
},

Erreurs courantes et solutions

EntityNotFoundException

Cette erreur survient quand une entite referencee en base n'existe plus (suppression sans cascade). Symfony ne peut pas hydrater le formulaire. Pour s'en premunir, ajoutez une contrainte de validation :

use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

->add('category', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name',
    'invalid_message' => 'Cette categorie n'existe plus.',
])

Formulaire vide malgre des donnees en base

Verifiez que le query_builder retourne bien des resultats. Un filtre trop restrictif peut exclure toutes les entites. Utilisez le Web Profiler pour inspecter les requetes SQL generees par le formulaire.

La valeur selectionnee ne se sauvegarde pas

Pour les relations ManyToMany, oubliez by_reference => false et vos methodes addXxx()/removeXxx() ne seront jamais appelees. Pour les formulaires imbriques, verifiez que la propriete est accessible via le PropertyAccessor.

Exemple complet : formulaire de projet

Voici un formulaire realiste combinant plusieurs EntityType pour la creation d'un projet. Ce type de formulaire est representatif de ce que vous trouverez dans un guide complet des FormType Symfony :

<?php

namespace App\Form;

use App\Entity\Project;
use App\Entity\Client;
use App\Entity\Technology;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ProjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => 'Nom du projet',
            ])
            ->add('description', TextareaType::class)
            ->add('client', EntityType::class, [
                'class' => Client::class,
                'choice_label' => fn (Client $c) => "{$c->getCompanyName()} ({$c->getCity()})",
                'placeholder' => 'Selectionner un client',
                'query_builder' => fn ($repo) => $repo->createQueryBuilder('c')
                    ->where('c.active = true')
                    ->orderBy('c.companyName', 'ASC'),
            ])
            ->add('leadDeveloper', EntityType::class, [
                'class' => User::class,
                'choice_label' => 'fullName',
                'query_builder' => fn ($repo) => $repo->createQueryBuilder('u')
                    ->where('u.roles LIKE :role')
                    ->setParameter('role', '%ROLE_DEV%')
                    ->orderBy('u.lastName', 'ASC'),
            ])
            ->add('technologies', EntityType::class, [
                'class' => Technology::class,
                'choice_label' => 'name',
                'multiple' => true,
                'expanded' => true,
                'by_reference' => false,
                'group_by' => fn (Technology $t) => $t->getType(),
            ]);
    }

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

Ce formulaire illustre les quatre patterns les plus courants d'EntityType : selection simple avec callback, filtrage par query_builder, selection multiple avec checkboxes, et regroupement par categorie. Maitrisez ces quatre patterns et vous couvrirez 95% des cas d'usage.

Besoin d'un formulaire Symfony complexe ou d'une application sur mesure ? En tant que developpeur freelance base a Metz et Strasbourg, j'accompagne des entreprises en France et en Belgique. Contactez-moi pour discuter de votre projet, consultez mes tarifs ou decouvrez mes services de developpement web.

Questions fréquentes

13 projets livrésGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tête ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler