Pourquoi il ne faut pas mettre de valeur par défaut dans les entités Doctrine

Quand vous créez des entités avec Doctrine vous avez peut-être des propriétés avec des valeurs par défaut et vous ne devriez pas le faire.

Jérémy 🤘
Jérémy 🤘

Quand vous créez des entités avec Doctrine vous avez peut-être des propriétés avec des valeurs par défaut et vous serez peut-être tenté de faire comme cela :

Copier
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\FooRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping AS ORM;

#[ORM\Entity(repositoryClass: FooRepository::class)]
class Foo {
    #[ORM\Column(type: Types::INTEGER)]
    #[ORM\Id, ORM\GeneratedValue(strategy: 'AUTO')]
    protected int $id;

    #[ORM\Column(type: Types::STRING)]
    protected string $title = 'foo';

    [...]
    // getters & setters
}
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\FooRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping AS ORM;

#[ORM\Entity(repositoryClass: FooRepository::class)]
class Foo {
    #[ORM\Column(type: Types::INTEGER)]
    #[ORM\Id, ORM\GeneratedValue(strategy: 'AUTO')]
    protected int $id;

    #[ORM\Column(type: Types::STRING)]
    protected string $title = 'foo';

    [...]
    // getters & setters
}

Déjà première erreur, la valeur par défaut n’est pas du tout enregistrée dans la base de données. Donc si je crée une entrée, sans passer par Doctrine, la valeur de la colonne foo devra obligatoirement être inscrite.

Et la seconde, la plus grosse selon moi, est le fait de mettre la valeur par défaut directement dans la propriété.

Pourquoi ? On va justement voir ça.

Requête partielle

Avec Doctrine il est possible de récupérer uniquement certains champs d’une entité grâce au mot-clé PARTIAL. Par défaut, quand on récupère une entité avec Doctrine, il va prendre tous les champs de notre entité et les hydrater (heureusement il ne fait pas un SELECT *).

Dans le cas où je fais SELECT PARTIAL f.{id} FROM App\Entity\Foo f et puis que je fais un appel à $foo->getTitle() je vais avoir foo comme retour, alors que je devrai avoir une erreur me disant que la variable n’est pas initialisée.

Pourquoi ? Tout simplement car Doctrine ne passe pas par les setters de notre entité, mais il fait une ReflectionClass de notre entité, passe les propriétés en accessible à true via ReflectionProperty et insère la donnée. Donc, dans le cas où je fais un PARTIAL, foo n’est pas récupéré dans la requête SQL et il ne devrait donc pas être initialisé.

Analyse du code

Dans le Unit of Work de Doctrine, au niveau de la méthode createEntity, aux lignes 2695 et 2744 on peut voir qu’il utilise la méthode setValue.

Et si on regarde d’où cela vient, on s’aperçoit qu’il existe deux classes, RuntimePublicReflectionProperty et TypedNoDefaultReflectionProperty, où toutes les deux extends de ReflectionProperty.

Conclusion

La bonne pratique est donc d’utiliser l’option de valeur par défaut proposé dans les attributs Doctrine et de mettre les valeurs par défaut de notre entité dans le constructeur de celle-ci.

Copier
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\FooRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping AS ORM;

#[ORM\Entity(repositoryClass: FooRepository::class)]
class Foo {
    #[ORM\Column(type: Types::INTEGER)]
    #[ORM\Id, ORM\GeneratedValue(strategy: 'AUTO')]
    protected int $id;
		
    #[ORM\Column(type: Types::STRING, options: ["default" : 'foo'])]
    protected string $title;

    public function __construct()
    {
        $this->title = 'foo';
    }

    [...]
    // getters & setters
}
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\FooRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping AS ORM;

#[ORM\Entity(repositoryClass: FooRepository::class)]
class Foo {
    #[ORM\Column(type: Types::INTEGER)]
    #[ORM\Id, ORM\GeneratedValue(strategy: 'AUTO')]
    protected int $id;
		
    #[ORM\Column(type: Types::STRING, options: ["default" : 'foo'])]
    protected string $title;

    public function __construct()
    {
        $this->title = 'foo';
    }

    [...]
    // getters & setters
}