Typensichere IDs mit Symfony und Doctrine: Verwendung dedizierter ID-Klassen
Lerne, wie du die Typensicherheit in Symfony und Doctrine durch die Verwendung dedizierter ID-Klassen wie BookId und UserId anstelle roher UUIDs verbessern kannst. Dieser Ansatz verhindert die Verwechslung von IDs, verbessert die Codeklarheit und sorgt für eine bessere Integration mit Symfony Messenger und Repository-Methoden. Entdecke praxisnahe Beispiele und Best Practices zur Implementierung typsicherer IDs in deinen Symfony-Anwendungen.
Beim Arbeiten mit Symfony und Doctrine ist die Verwendung von UUIDs als Entitätskennungen eine gängige Vorgehensweise. Traditionell werden IDs als einfache Ganzzahlen oder als rohe Uuid-Objekte gespeichert. Dies kann jedoch zu Verwechslungen führen, insbesondere beim Arbeiten mit Symfony Messenger oder Repository-Methoden. Ein robusterer und typsicherer Ansatz besteht darin, dedizierte ID-Klassen zu verwenden.
Warum dedizierte ID-Klassen verwenden?
Durch die Verwendung einer dedizierten ID-Klasse, wie BookId
oder UserId
, wird sichergestellt, dass Kennungen nicht versehentlich vertauscht werden. Dies ist besonders nützlich beim Arbeiten mit Symfony Messenger-Nachrichten, in denen mehrere UUIDs übergeben werden. Zum Beispiel:
namespace App\Message;
final readonly class MarkBookAsRead
{
public function __construct(
public BookId $bookId,
public UserId $userId,
) {}
}
Mit diesem Ansatz wird sichergestellt, dass eine BookId
niemals mit einer UserId
verwechselt wird. Würden wir generische UUIDs verwenden, könnten die Parameter leicht vertauscht werden, was zu schwer Fehlern führen kann.
Implementierung einer dedizierten ID-Klasse
So kann eine dedizierte ID-Klasse für ein Book
-Entity definiert werden:
namespace App\Domain;
use Symfony\Component\Uid\Ulid;
final class BookId extends Ulid
{
}
Nun kann diese Klasse im Book
-Entity verwendet werden:
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;
}
// ...
}
Der benötigte Doctrine-Typ
Das letzte Code-Beispiel liefert uns folgende Exception:
Cannot assign Symfony\Component\Uid\Ulid to property App\Entity\Book::$id of type App\Entity\BookId
Um das zu beheben, benötigen wir einen BookIdType
:
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;
}
}
welcher registriert und benutzt werden muss:
# config/packages/doctrine.yaml
doctrine:
dbal:
types:
App\Doctrine\Type\BookIdType: App\Doctrine\Type\BookIdType
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;
}
// ...
}
Typsichere Repository-Methoden
Ein weiterer Vorteil sind typsichere Repository-Methoden. Anstatt Entities mit einer generischen string
-UUID abzufragen, kann die Typensicherheit auf Methodenniveau erzwungen werden:
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;
}
}
Und eine aussagekräftige Exception definieren:
namespace App\Exception;
use App\Domain\BookId;
final class BookNotFoundException extends \RuntimeException
{
public static function withId(BookId $id): self
{
return new self(sprintf('Buch mit der ID %s nicht gefunden.', $id->toString()));
}
}
Fazit
Die Verwendung dedizierter ID-Klassen in Symfony mit Doctrine bietet Typensicherheit, reduziert das Risiko von Verwechslungen bei Kennungen und verbessert die Codeklarheit. Dies ist besonders nützlich beim Arbeiten mit Symfony Messenger und Repository-Methoden, wo eine falsche ID-Handhabung zu Laufzeitfehlern führen kann. Durch die Implementierung dedizierter ID-Klassen entsteht eine robustere und besser wartbare Codebasis.