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
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.
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é.