Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Id Type Laravel Package

skrepr/id-type

Skrepr ID Type adds type-safe ID value objects to Laravel Eloquent models, with a custom cast for UUIDs, ULIDs, and other identifiers so you can validate, serialize, and use IDs consistently across your application. Lightweight and easy to configure.

View on GitHub
Deep Wiki
Context7

Getting Started

skrepr/id-type solves one specific problem: replacing primitive int/string identifiers on Doctrine entities with strongly-typed ID value objects (UserId, OrderId, …) without hand-writing a DBAL type, hydrator, or serializer for every one of them.

Minimal path:

  1. composer require skrepr/id-type
  2. Create one class per identifier, extending the package's abstract ID base:
    final class UserId extends AbstractIdType {}
    
  3. Register the DBAL type so Doctrine knows how to map it to the database column. In a Laravel app this usually lives in a service provider wiring Doctrine's Type::addType() / the doctrine config block (Laravel's Eloquent has no equivalent — this package is for the Doctrine ORM side of a Laravel codebase, or for a Symfony/standalone service).
  4. Put it on the entity:
    #[ORM\Id]
    #[ORM\Column(type: UserId::class)]
    private UserId $id;
    

Where to look first: the abstract ID base class (that's where generate(), fromString(), and __toString() semantics are defined), then the registered DBAL type — those two files answer almost every question you'll have. Check the package's own test suite for the canonical entity mapping example rather than guessing at attribute syntax.

First real use case: swap the id of a single aggregate root from int to a typed ID, generate it in the constructor ($this->id = UserId::generate();), and confirm a round-trip (persist → clear → find) returns an instance, not a raw string. Do this on one entity before touching the rest of the model.

Implementation Patterns

One ID class per aggregate, not per entity. UserId, OrderId, InvoiceId — never a shared GenericId. The whole value of the package is that a method signature find(OrderId $id) cannot be called with a UserId by accident. The moment you share a base class across aggregates you lose that.

Generate at construction, never at persistence. Assign in the entity constructor (or a named factory) so the ID exists before flush(). This keeps the entity valid in memory, makes events dispatched pre-persist referenceable, and avoids the "when is the ID null?" branch that spreads through your codebase.

Type-hint IDs in repositories and services. Use find(OrderId $id): ?Order on your repository rather than find($id). This is where the package pays for itself — static analysis catches ID mix-ups at the call site instead of at runtime after a bad join.

EntityValueResolver for controller input. Where you use Symfony's EntityValueResolver (or the Laravel equivalent via a Doctrine bridge), typed IDs let route parameters convert straight into entities without manual find() plumbing in every controller action. This is exactly the path 1.3.2 tightened for Doctrine ORM 3 — see Gotchas.

Serialization boundary. Expose (string) $id in API resources / JSON responses, and accept a string on the way in, converting at the edge (UserId::fromString($request->input('id'))) so the ID object never leaks into request validation rules or Blade templates. Keep fromString() calls at the boundary and let the domain work with objects.

Testing seam. Because IDs are constructible from a known string, tests can assert on fixed IDs (OrderId::fromString('...')) instead of capturing generated values. Build a small helper/factory for deterministic IDs; it removes most of the friction of testing entities keyed by generated identifiers.

Migration discipline. All ID columns should share one physical representation (char(36) for UUID-style, or binary(16) if the package is configured that way). Decide once, document it, and add new entities to that convention — mixed representations across tables is painful to unwind later.

Gotchas and Tips

1.3.2 specifically: EntityValueResolver + doctrine/orm:^3. This release is a compatibility fix, not a behaviour change. If you pinned doctrine/orm to ^2.x you are unaffected. If you are on ORM 3 and were seeing entity resolution fail or misbehave in controllers (typically the resolver not converting a route parameter into the expected entity, or falling back to a find() with the wrong type), 1.3.2 is the version to be on. Upgrade, then re-run your functional/controller tests for any action that relies on implicit entity resolution — that's the surface area this touched.

Type registration must match the #[ORM\Column(type: ...)] string exactly. The most common failure mode is registering the DBAL type under the short name user_id but annotating the column with the FQCN (or vice versa). Symptoms are confusing: Doctrine may silently treat the column as string and hydrate a string rather than throwing. If hydration returns a raw string instead of your ID object, check registration first.

Equality is not object identity. Two UserId instances built from the same UUID are not ===. Compare with ->equals() if the base provides it, or (string) $a === (string) $b. This bites in in_array() (without strict flag), array_unique, and Doctrine's identity map assertions in tests.

Doctrine's identity map keys on the DBAL-converted value. If your conversion is inconsistent (e.g. casing differences between fromString() and what the DB returns), you can end up with two in-memory instances of the same row. Normalise on the way in and out.

Missing ID on a new entity fails at flush, not at construction. If you forget to assign in the constructor, PHP will throw Typed property must not be accessed before initialization — usually from deep inside Doctrine's UnitOfWork, which obscures the cause. Initialise eagerly in the constructor.

Keep ID classes final and immutable. No setters, no public mutable state. The package's extension points are the DBAL type and the base class — subclass the base for a new ID, don't try to make an existing ID class configurable after the fact.

Schema length matters. A 36-char UUID in a VARCHAR(32) column truncates silently on some drivers. Verify the migration column definition matches the package's expected string length before you have production data.

Debugging tip: when an entity mysteriously won't persist or is treated as new, dump $entity->getId() type — if it's string, your type conversion isn't wired and Doctrine is comparing a fresh string ID against a persisted object ID, so it always sees a new entity.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect