ericgansa / ghost-trees-bundle
Pattern d'héritage dynamique pour entités Doctrine — les arbres fantômes.
Package info
github.com/Magikmagnum/gost-trees-bundle
Type:symfony-bundle
pkg:composer/ericgansa/ghost-trees-bundle
Requires
- php: >=8.2
- doctrine/doctrine-bundle: ^2.10
- doctrine/orm: ^2.17 || ^3.0
- symfony/console: ^6.4 || ^7.0
- symfony/framework-bundle: ^6.4 || ^7.0
- symfony/validator: ^6.4 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.0
- symfony/phpunit-bridge: ^7.0
- symfony/yaml: ^6.4 || ^7.0
This package is not auto-updated.
Last update: 2026-05-03 14:40:54 UTC
README
Héritage dynamique pour entités Doctrine. Les arbres fantômes permettent à une entité enfant d'hériter dynamiquement des attributs d'une entité parente, attribut par attribut, sans duplication de données et avec propagation automatique.
Le pitch en 30 secondes
Vous avez une entité dont l'état doit être partagé avec d'autres entités, mais chacune doit pouvoir personnaliser localement certains attributs sans perdre le lien avec l'original ?
Au lieu de cloner ou de référencer en dur, vous créez un fantôme : une entité qui pointe vers un parent et n'écrit localement que ce qui diverge. Tant qu'un attribut reste à null localement, il est résolu dynamiquement depuis le parent.
Racine T1 : lieuDepart = "Paris" lieuArrivee = "Lyon" moyenTransport = "TGV"
↑ ↑
│ │
Fantôme T1n : lieuDepart = null lieuArrivee = "Marseille" moyenTransport = null
│
└─→ Lecture résolue : "Paris" "Marseille" "TGV"
Effacer la valeur locale "Marseille" rétablit automatiquement la lecture transparente vers "Lyon". Aucune copie. Aucun cache. Aucun événement.
Installation
composer require ericgansa/ghost-trees-bundle
Ajoutez le bundle à config/bundles.php (Flex le fait pour vous) :
return [ // ... EricGansa\GhostTreesBundle\GhostTreesBundle::class => ['all' => true], ];
Configuration
# config/packages/ghost_trees.yaml ghost_trees: max_depth: 1 # Profondeur maximale (1 = racine + fantômes) on_root_delete: cascade # cascade | incarnate auto_propagate_collections: true
Rendre une entité fantomisable
Deux étapes :
- Implémenter
GhostableInterfaceet utiliserGhostNodeTrait— le trait fournit$parent,getParent(),setParent(),isGhost()etresolve(). - Marquer les attributs fantomisables avec
#[GhostableField], et faire passer leurs getters parresolve().
Pour une entité Doctrine, il suffit en plus de redéclarer la propriété $parent avec son mapping (le trait ne peut pas le faire à votre place — Doctrine a besoin de la classe concrète comme targetEntity).
use EricGansa\GhostTreesBundle\Attribute\GhostableField; use EricGansa\GhostTreesBundle\Contract\GhostableInterface; use EricGansa\GhostTreesBundle\Trait\GhostNodeTrait; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class Trajet implements GhostableInterface { use GhostNodeTrait; #[ORM\Id, ORM\GeneratedValue, ORM\Column] private ?int $id = null; #[ORM\Column(length: 255, nullable: true)] #[GhostableField(required: true)] private ?string $lieuDepart = null; // Redéclaration nécessaire UNIQUEMENT pour le mapping Doctrine. #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] protected ?GhostableInterface $parent = null; #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)] private Collection $children; public function __construct() { $this->children = new ArrayCollection(); } public function getLieuDepart(): ?string { return $this->resolve($this->lieuDepart, 'getLieuDepart'); } public function setLieuDepart(?string $lieuDepart): static { $this->lieuDepart = $lieuDepart; return $this; } }
Pour un usage sans Doctrine (DTO, fixtures de test, entités en mémoire), aucune redéclaration n'est nécessaire : le trait fonctionne tel quel.
Vocabulaire
| Terme | Sens |
|---|---|
| Racine | Entité sans parent. Source des valeurs originales. |
| Fantôme | Entité avec un parent. Hérite dynamiquement des valeurs. |
| Matérialisation | Action de donner une valeur locale à un attribut fantôme. |
| Dématérialisation | Action d'effacer (null) une valeur locale. La résolution dynamique reprend. |
| Incarnation | Promotion d'un fantôme en racine autonome. Toutes les valeurs sont matérialisées et le lien parent coupé. |
| Traversée | Lecture d'un attribut qui remonte la chaîne fantôme jusqu'à trouver une valeur. |
Outillage
# Inspecter l'état de résolution d'une entité fantôme php bin/console debug:ghosts "App\Entity\Trajet" 42 # Incarner un fantôme en racine autonome php bin/console ghosts:incarnate "App\Entity\Trajet" 42
Invariants garantis
- Profondeur : aucune chaîne fantôme ne dépasse
max_depth. - Pas de cycle : une entité ne peut pas être son propre ancêtre.
- Transparence de lecture : un fantôme non matérialisé renvoie les valeurs du parent.
- Isolation d'écriture : modifier un fantôme n'affecte jamais le parent.
- Réversibilité : effacer une valeur locale (
null) restaure la résolution dynamique.
Documentation
Licence
MIT.