End of ID Chaos: Type-Safe Identifiers in Symfony and Doctrine (Automated)
When working with Symfony and Doctrine, it is common practice today to use UUIDs or ULIDs as entity identifiers. Most of the time, we…
End of ID Chaos: Type-Safe Identifiers in Symfony and Doctrine (Automated)

When working with Symfony and Doctrine, it is common practice today to use UUIDs or ULIDs as entity identifiers. Most of the time, we represent them in code as strings or raw objects of type Uuid or Ulid.
This approach is functional, but it brings with it one subtle risk: type confusion.
The Problem: When Everything is “Just” a UUID
Imagine a method in Symfony Messenger or in a Controller that processes an order for a specific customer:
// ❌ PROBLEM: Both parameters are technically the same (string/ULID)
final readonly class ProcessOrder
{
public function __construct(
public string $orderId, // e.g. "01JK..."
public string $customerId, // e.g. "01JK..."
) {}
}
// In coding, this can easily happen:
$bus->dispatch(new ProcessOrder(
$customer->getId(), // ⚠️ OOPS! We swapped the order
$order->getId()
));p
Both parameters are technically strings. If you accidentally swap them when calling the constructor, PHP will not warn you. The application will attempt to find the order using the customer ID, resulting in an error that is sometimes not easy to detect immediately.
Oskar Stark, in his recent article on the SensioLabs blog, suggests a solution: Custom ID classes for each entity.
The Solution: Type-Safe Identifiers
Instead of a generic string or Ulid, we use dedicated classes OrderId and CustomerId.
// ✅ SOLUTION: Each ID has its own type
final readonly class ProcessOrder
{
public function __construct(
public OrderId $orderId,
public CustomerId $customerId,
) {}
}
// PHPStan or PHP itself will now stop you:
$bus->dispatch(new ProcessOrder(
$customerId, // 🔴 FATAL ERROR: Argument #1 must be of type OrderId, CustomerId given
$orderId
));
The Manual Way: Lots of “Boilerplate” Code
While this idea is great, implementing it in pure Symfony requires several steps for every single entity:
- Create an ID class (e.g.,
BookIdextendingUlid). - Create a custom Doctrine Type (e.g.,
BookIdType). - Register this type in
doctrine.yaml. - Modify the entity to use this new type in the
#[ORM\Column].
For one entity, it’s fine. However, for a project with 20 entities, it’s repetitive and tedious work.
Our Solution: Type-Safe Id Bundle
That’s why we created the **Type-Safe Id Bundle**. Our goal was to take this correct architectural idea and make it almost invisible to the developer.
This bundle createnew command make:entity:type . When you create an entity, the bundle automatically generates everything you need for you.
How does it work in practice?
Just use the familiar command with an added flag:
bin/console make:entity:type Product --with-ulid
The bundle automatically generates this code for you:
- ID Class (
src/EntityId/ProductId.php):
namespace App\EntityId;
use Symfony\Component\Uid\Ulid;
final class ProductId extends Ulid {
public static function new(): self
{
return new self();
}
}
- Entity with Type-Safe ID:
#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.id_generator.universal')]
#[ORM\Column(type: ProductIdType::class, unique: true)]
private ProductId $id;
public function __construct()
{
$this->id = new ProductId();
}
public function getId(): ProductId
{
return $this->id;
}
}
- Enhanced Repository:
class ProductRepository extends ServiceEntityRepository
{
public function get(ProductId $id): ?Product
{
return $this->find($id->toString());
}
}
Why start using Type-Safe IDs today?
- Elimination of logic errors: Swapping the IDs of two different entities is physically impossible.
- Cleaner code: Type-hints in constructors and methods clearly state what they expect.
- Better DX (Developer Experience): With our bundle, you don’t have to write any extra code. The
make:entitycommand does all the "dirty work" for you. - Easy integration: The bundle integrates directly into the Symfony workflow you are used to.
Installation
If you want to try type-safe identifiers without unnecessary coding, install our bundle via Composer:
composer require tito10047/type-safe-id-bundle
Then enable it in config/bundles.php (if you don't use Flex), and you can start generating safe entities.
This bundle is currently in an experimental phase; we welcome any feedback or PRs on GitHub.
메타데이터
- post_id
- 71bee96cb371
- slug
- end-of-id-chaos-type-safe-identifiers-in-symfony-and-doctrine-automated-71bee96cb371
- url
- https://medium.com/@mostka.j/end-of-id-chaos-type-safe-identifiers-in-symfony-and-doctrine-automated-71bee96cb371
- canonical_url
- https://medium.com/@mostka.j/end-of-id-chaos-type-safe-identifiers-in-symfony-and-doctrine-automated-71bee96cb371
- author_url
- https://medium.com/@mostka.j
- status
- ok
- fetched_at
- 2026-07-13 06:23:13