SymfonyLive Paris 2025, la plus grande conférence Symfony en français, a lieu les 27 et 28 mars 🥐 Réservez votre billet ici dès maintenant


Des identifiants sécurisés par type avec Symfony et Doctrine : l'utilisation de classes d’ID dédiées

· Oskar Stark · Temps de lecture: 2 minutes
type-safety-uuid

Apprenez à améliorer la sécurité des types dans Symfony et Doctrine en utilisant des classes d’ID dédiées comme BookId et UserId au lieu d’UUID bruts. Cette approche permet d'éviter la confusion des identifiants, de rendre le code plus clair et d'assurer une meilleure intégration avec Symfony Messenger et les méthodes de repository. Découvrez des exemples pratiques et les meilleures pratiques pour implémenter des identifiants sécurisés par type dans vos applications Symfony.

L'utilisation d'UUIDs comme identifiants d'entité est une approche courante avec Symfony et Doctrine. En général, les IDs sont stockés sous forme de nombres entiers simples ou d'objets UUID bruts. Cependant, cela peut entraîner une confusion des types, en particulier lors de l'utilisation de Symfony Messenger ou des méthodes de repository. Une approche plus robuste et plus sûre consiste à utiliser des classes d'ID dédiées.

Pourquoi utiliser des classes d'ID dédiées ?

L'utilisation d'une classe d'ID dédiée, comme BookId ou UserId, garantit que les identifiants ne soient pas accidentellement confondus. C'est particulièrement utile pour la gestion de messages avec Symfony Messenger, où plusieurs UUIDs peuvent être passés. Par exemple :

namespace App\Message;

final readonly class MarkBookAsRead
{
    public function __construct(
        public BookId $bookId,
        public UserId $userId,
    ) {}
}

Avec cette approche, nous nous assurons qu'un BookId ne sera jamais confondu avec un UserId. Si nous utilisions des UUIDs bruts, les paramètres pourraient être facilement inversés, ce qui entraînerait des erreurs difficiles à diagnostiquer.

Implémenter une classe d'ID dédiée

Voici comment définir une classe d'ID dédiée pour une entité Book :

namespace App\Domain;

use Symfony\Component\Uid\Ulid;

final class BookId extends Ulid
{
}

Désormais, dans votre entité Book, vous pouvez utiliser cette classe comme identifiant :

namespace App\Entity;

use App\Domain\BookId;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Book
{
    #[ORM\Id]
    #[ORM\Column(type: 'uuid', unique: true)]
    private BookId $id;

    public function __construct()
    {
        $this->id = new BookId();
    }

    public function getId(): BookId
    {
        return $this->id;
    }

    // ...
}

Le type Doctrine nécessaire

Ce dernier exemple entraînera l’exception suivante dans le code :

Cannot assign Symfony\Component\Uid\Ulid to property App\Entity\Book::$id of type App\Entity\BookiId

Pour le corriger, un BookIdType est nécessaire :

namespace App\Doctrine\Type;

use App\Domain\BookId;
use Symfony\Bridge\Doctrine\Types\AbstractUidType;

final class BookIdType extends AbstractUidType
{
    public function getName(): string
    {
        return self::class;
    }

    protected function getUidClass(): string
    {
        return BookId::class;
    }
}

Ce type doit ensuite être enregistré et utilisé :

# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            App\Doctrine\Type\BookIdType: App\Doctrine\Type\BookIdType

Dans l’entité Book :

namespace App\Entity;

+use App\Doctrine\Type\BookIdType;
use App\Domain\BookId;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Book
{
    #[ORM\Id]
-   #[ORM\Column(type: 'ulid', unique: true)]
+   #[ORM\Column(type: BookIdType::class, unique: true)]

    private BookId $id;

    public function __construct()
    {
        $this->id = new BookId();
    }

    public function getId(): BookId
    {
        return $this->id;
    }

    // ...
}

Les méthodes de repository avec typage fort

Un autre avantage est l'utilisation de méthodes de repository avec typage fort. Plutôt que de récupérer des entités via un string UUID générique, vous pouvez appliquer la sécurité des types au niveau des méthodes :

namespace App\Repository;

use App\Domain\BookId;
use App\Entity\Book;

final class BookRepository
{
    public function get(BookId $id): Book
    {
        $book = $this->find($id->toString());
        if (null === $book) {
            throw BookNotFoundException::withId($id);
        }
        return $book;
    }
}

Et définir une exception explicite :

namespace App\Exception;

use App\Domain\BookId;

final class BookNotFoundException extends \RuntimeException
{
    public static function withId(BookId $id): self
    {
        return new self(sprintf('Livre avec l’ID %s introuvable.', $id->toString()));
    }
}

Conclusion

L'utilisation de classes d'ID dédiées dans Symfony avec Doctrine améliore la sécurité des types, réduit le risque de confusion des identifiants et améliore la clarté du code. C'est particulièrement utile lors de l'utilisation de Symfony Messenger et des méthodes de repository, où une mauvaise gestion des IDs peut entraîner des erreurs à l'exécution. En implémentant des classes d'ID dédiées, vous créez une base de code plus robuste et plus facile à maintenir.

Utilisez dès maintenant des classes d’ID

SensioLabs aide votre équipe à adopter les meilleures pratiques pour développer des projets maintenables et évolutifsContactez-nous dès maintenant pour en savoir plus !

Image