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

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.