C'est quoi exactement ?

Un Embeddable est une classe PHP annotée #[ORM\Embeddable]. Elle regroupe un ensemble de colonnes liées logiquement. Côté base de données, ces colonnes sont ajoutées directement dans la table de l'entité qui l'utilise, aucune table supplémentaire à ajouter.

Analogie : pensez à un objet Adresse avec rue, ville, codePostal. Plutôt que de répéter ces 3 colonnes dans chaque entité Client, Fournisseur, etc., vous les déclarez une seule fois dans un Embeddable et vous le branchez où vous voulez.

Un exemple concret

Imaginons qu'une application affiche des popups de confirmation à différents endroits (validation d'étape, inscription, etc.). Chaque popup a les mêmes besoins : un titre, un contenu, deux boutons. On crée un Embeddable une seule fois :

src/Entity/Embeddable/PopupConfig.php
#[ORM\Embeddable]
class PopupConfig
{

#[ORM\Column(type: 'boolean', options: ['default' => false])]
private bool $enabled = false;


#[ORM\Column(type: 'string', length: 255, nullable: true)]
private ?string $title = null;


#[ORM\Column(type: 'text', nullable: true)]
private ?string $content = null;


#[ORM\Column(type: 'string', length: 127, nullable: true)]
private ?string $labelConfirm = null;


#[ORM\Column(type: 'string', length: 127, nullable: true)]
private ?string $labelCancel = null;


// getters / setters...
}

Ensuite, dans l'entité qui en a besoin, on l'intègre avec #[ORM\Embedded] :

src/Entity/RegistrationStep.php (extrait)
#[ORM\Embedded(
class: PopupConfig::class,
columnPrefix: 'validation_popup_'
)]

private PopupConfig $validationPopup;

Le paramètre columnPrefix permet d'avoir plusieurs Embeddables du même type dans la même entité, sans collision de noms de colonnes. Ici, les colonnes s'appelleront validation_popup_enabled, validation_popup_title, etc.

Ce que ça donne en base de données

Pas de nouvelle table. Les colonnes de PopupConfig sont simplement ajoutées à la table de l'entité parente, préfixées :

table: registration_step
┌─────────────────────────────────────┐
│ id                                  │
│ ...autres colonnes de l'entité...   │
│ validation_popup_enabled            │
│ validation_popup_title              │
│ validation_popup_content            │
│ validation_popup_label_confirm      │
│ validation_popup_label_cancel       │
└─────────────────────────────────────┘

Réutiliser le même Embeddable plusieurs fois

C'est là que ça devient vraiment intéressant. Si l'entité a besoin de deux popups distinctes (validation ET annulation par exemple), il suffit de déclarer deux propriétés avec des préfixes différents :

#[ORM\Embedded(class: PopupConfig::class, columnPrefix: 'validation_popup_')]
private PopupConfig $validationPopup;

#[ORM\Embedded(class: PopupConfig::class, columnPrefix: 'cancel_popup_')]
private PopupConfig $cancelPopup;

Deux jeux de colonnes séparés, un seul objet PHP à maintenir. Le gain de cohérence est immédiat.

Avantages et limites

Ce qu'on y gagne

  • Pas de duplication de code
  • Objet PHP cohérent et typé
  • Pas de JOIN en base, performances identiques
  • Facilement testable de manière isolée
  • Compatible JMS Serializer et groupes de sérialisation

Ce qu'il faut savoir

  • Impossible de faire des requêtes complexes sur l'Embeddable seul
  • Pas de relation ManyToOne/OneToMany dans un Embeddable
  • Les colonnes nullable peuvent augmenter si l'Embeddable est optionnel
  • Migrations à surveiller si le préfixe change

Quand l'utiliser ?

✓ Bon usage

  • Configuration réutilisée (popup, email, notification...)
  • Données de contact (adresse, téléphone...)
  • Coordonnées géographiques
  • Plage de dates (dateStart / dateEnd)

✗ À éviter si...

  • Vous avez besoin d'une clé primaire propre
  • Le groupe de champs est utilisé dans des relations
  • Vous voulez requêter ces données indépendamment
En résumé

Les Embeddables Doctrine sont un outil puissant pour encapsuler un groupe de colonnes logiquement liées dans un objet PHP réutilisable. Ils gardent votre schéma simple (une seule table), votre code DRY, et vos objets expressifs. Dès que vous vous surprenez à copier-coller les mêmes 3-5 champs dans plusieurs entités, c'est le signe qu'un Embeddable s'impose.

Embeddable vs Trait — lequel choisir ?

En PHP/Symfony, les Traits sont souvent utilisés pour partager des champs entre entités (pensez au classique TimestampableTrait avec createdAt et updatedAt). À première vue, ils semblent faire la même chose que les Embeddables. Voici pourquoi ce n'est pas tout à fait le cas.

CritèreTraitEmbeddable
Structure PHPMixin — les propriétés sont copiées dans la classeObjet autonome avec sa propre classe
TypagePas d'objet propre, pas de type hint possibleObjet typé, injectable, testable seul
RéutilisationUne instance par entité qui use le traitPlusieurs fois dans la même entité (avec préfixes)
Logique métierPossible mais vite difficile à maintenirEncapsulée proprement dans la classe Embeddable
Tests unitairesNécessite une entité pour testerLa classe Embeddable se teste seule
Cas typiqueTimestamps, soft delete, colonnes transverses simplesConfig réutilisable, adresse, plage de dates
Règle simple

Utilisez un Trait pour des colonnes techniques et transverses (createdAt, deletedAt...) qui n'ont pas de logique propre. Préférez un Embeddable dès que le groupe de champs a un sens métier autonome et pourrait apparaître plusieurs fois dans la même entité.