PHP 8 et Symfony : la révolution discrète qui change tout
PHP 8 & Symfony : la révolution des attributs qui unifie le code et la configuration.
Il était une entité Doctrine avec ces 200 lignes de getters/setters pour 10 propriétés. L’époque des cris et des pleurs lors d’une refacto est désormais révolue avec la sortie de PHP 8.4 et l’introduction de ces fameux Property Hooks avec Doctrine 3.4.
Les Property Hooks, c'est la réponse de PHP à tous ceux qui jalousaient C# et ses properties depuis 2002. Oui, on a mis 22 ans à rattraper le coup, mais il vaut mieux tard que jamais.
En gros, au lieu d'écrire ça :
class User {
private string $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
}
Vous pouvez maintenant faire ça :
class User {
public string $name;
}
Et là vous me dites « Tu bluffes Martoni, ça existe depuis PHP 4 ». C’est là qu’entrent que les hooks entrent en jeu :
class User {
public string $name {
get => strtoupper($this->name);
set => $this->name = trim($value);
}
}
On peut intégrer de la logique custom, syntaxe élégante et vos yeux ne saignent plus avec les 500 lignes des getters et setters : tout est au même endroit.
Le 28 juin 2025, l'équipe Doctrine fait l’annonce : Doctrine ORM 3.4 supporte officiellement les Property Hooks.
Avant Doctrine 3.4, vos entités ressemblaient à ça :
#[ORM\Entity]
class Post {
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private ?Uuid $id = null;
#[ORM\Column(length: 255)]
private string $title = '';
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
public function getId(): ?Uuid
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): static
{
$this->content = $content;
return $this;
}
}
Avec Doctrine 3.4 et PHP 8.4, voici la même entité :
#[ORM\Entity]
class Post {
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
public ?Uuid $id = null;
#[ORM\Column(length: 255)]
public string $title = '';
#[ORM\Column(type: Types::TEXT)]
public ?string $content = null;
}
60% de code en moins. Plus de getters/setters dans le trait = moins de conflits potentiels avec les classes qui l'utilisent. Win-win.
D'abord, le classique :
{
"require": {
"doctrine/orm": "^3.5", // La 3.5 était sortie donc c'est bonus
"doctrine/doctrine-bundle": "^2.15"
}
}
Et ensuite :
composer update doctrine/orm doctrine/doctrine-bundle
J'ai commencé par scanner toutes mes entités. Le bonheur [ non ].
Voici mon approche systématique :
Prenons l'exemple de mon entité Category :
Avant :
class Category extends AbstractContent
{
#[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'category')]
private Collection $posts;
public function __construct()
{
parent::__construct();
$this->posts = new ArrayCollection();
}
public function getPosts(): Collection
{
return $this->posts;
}
public function addPost(Post $post): static
{
if (!$this->posts->contains($post)) {
$this->posts->add($post);
$post->setCategory($this);
}
return $this;
}
public function removePost(Post $post): static
{
if ($this->posts->removeElement($post) && $post->getCategory() === $this) {
$post->setCategory(null);
}
return $this;
}
}
Après :
class Category extends AbstractContent
{
#[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'category')]
public Collection $posts;
}
C'est pas plus compliqué que ça !
L'entité AbstractContent qui était assez verbeuse :
abstract class AbstractContent implements \Stringable
{
#[ORM\Column(type: 'uuid', unique: true)]
#[ORM\Id]
public ?Uuid $id;
#[ORM\Column(length: 255)]
public string $title = '';
#[ORM\Column(length: 255)]
public string $slug = '';
#[ORM\Column(type: Types::TEXT, nullable: true)]
public ?string $content = null;
#[ORM\Column(nullable: true, enumType: ColorEnum::class)]
public ?ColorEnum $color = ColorEnum::BLUE;
#[ORM\Column(type: Types::STRING, enumType: ContentStatusEnum::class)]
public ContentStatusEnum $status = ContentStatusEnum::DRAFT;
#[ORM\Embedded(class: Seo::class)]
public Seo $seo;
public function __construct()
{
$this->seo = new Seo();
$this->id = Uuid::v4();
}
}
Et c’est tout !
#[ORM\Embeddable]
class Seo
{
#[ORM\Column(length: 255, nullable: true)]
public ?string $title = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
public ?string $description = null;
#[ORM\Column(nullable: true)]
public ?array $keywords = null;
#[ORM\Column(length: 255, nullable: true)]
public ?string $robots = 'index, follow';
}
Plus de getters/setters ici non plus. L'accès se fait maintenant via : $post->seo->title au lieu de $post->getSeo()->getTitle(). C'est beau, on dirait presque du code moderne.
class Post extends AbstractContent
{
#[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id')]
#[ORM\ManyToOne(inversedBy: 'posts')]
public ?Category $category = null {
set(?Category $value) {
$value?->posts->add($this);
$this->category = $value;
}
}
}
Plus de prise de tête avec les ad() et remove().
C'est là que ça devient sportif. Tous les endroits où vous appeliez des getters/setters doivent être mis à jour. Mon approche :
->get et ->set dans tout le projetExemples concrets : Avant :
$category = $this->categoryService->getCategoryBySlug($slug);
$posts = $category->getPosts();
$title = $category->getTitle();
Après :
$category = $this->categoryService->getCategoryBySlug($slug);
$posts = $category->posts;
$title = $category->title;
Bien entendu, après avoir testé ce changement de patterns, je me suis plus ou moins casser les dents sur certains points :
Si vous démarrez un nouveau projet en PHP 8.4+, essayer les Property Hooks, c’est les adopter.
Si vous avez un projet existant, évaluez le ROI. Pour un petit projet, la migration peut se faire en un week-end. Pour un monstre enterprise avec 500 entités, prévoyez une stratégie de migration progressive.
Dans tous les cas, les Property Hooks ne sont pas qu'un gadget syntaxique. C'est une évolution fondamentale de la façon dont on écrit du PHP orienté objet.
PS : Si vous trouvez des bugs dans votre migration, rappelez-vous : ce ne sont pas des bugs, ce sont des "opportunités d'apprentissage non documentées". Et si ça casse vraiment tout, git revert est votre ami.
PPS : Pour les puristes qui vont me dire que les getters/setters c'est de l'encapsulation et que c'est sacré : oui, mais non. L'encapsulation, c'est contrôler l'accès et la modification. Les hooks font exactement ça, juste avec 80% de code en moins. Deal with it.