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

Doctrine Generic Types Laravel Package

vanio/doctrine-generic-types

Adds support for generic types in Doctrine mappings, letting you declare typed collections and associations for better static analysis and IDE autocompletion. Integrates with PHPStan and Psalm to catch type errors in your entities early, with minimal setup and no changes to how your entities behave...

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small DBAL extension, so treat it as plumbing rather than a framework. The whole point is to let a single Doctrine column type be parameterised — you write something like collection<App\Money> in your mapping instead of inventing money_collection, tag_collection, uuid_collection as separate registered types.

1. Install

composer require vanio/doctrine-generic-types

Note that this only makes sense if you're already running Doctrine ORM in Laravel (e.g. laravel-doctrine/orm). If your app is Eloquent-first, the cost of adoption is the ORM, not this package — evaluate that first.

2. Read the source before the README

With 0 stars and 0 dependents there is no blog-post trail to lean on. First stop should be src/ — you're looking for two things:

  • the base Type class (or trait) that knows how to parse a type name and delegate to the inner type;
  • the registration/bootstrap entry point, if one exists, since you'll otherwise register types yourself.

3. Register the type(s) once, centrally

In Laravel, do it in a service provider, not in a model or a console command:

// app/Providers/DoctrineServiceProvider.php
use Doctrine\DBAL\Types\Type;

public function boot(): void
{
    if (! Type::hasType('collection')) {
        Type::addType('collection', \Vanio\DoctrineGenericTypes\...\CollectionType::class);
    }
}

Everything downstream — schema tool, migrations diff, hydrators, query builders — resolves DBAL types lazily at metadata-load time, so this must run before any entity metadata is touched.

4. First use case to try

Pick a value object you already store as JSON today and make it typed:

#[ORM\Column(type: 'collection<App\Value\Money>', nullable: true)]
private array $lineItems = [];

If that round-trips (persist → flush → reload → equals), you've validated the core mechanic and can expand. Don't start with relations or identifiers; start with a nullable JSON-ish column you can eyeball in the DB.

Implementation Patterns

Base type + registered instances. The idiomatic shape is one generic base class (parsing name<arg>, delegating conversion to the inner DBAL type or to a serializer) plus a thin registry so you never type the string by hand:

final class ColumnType
{
    public const MONEY_COLLECTION = 'collection<App\Value\Money>';
    public const UUID_SET = 'collection<Ramsey\Uuid\UuidInterface>';
}

Then #[ORM\Column(type: ColumnType::MONEY_COLLECTION)]. This gives you one place to grep when the inner class is renamed — a real concern when the inner class is a PHP FQCN embedded in a DB comment.

Value-object columns. The highest-leverage pattern day to day: a Money, GeoPoint, DateRange, or Slug type that converts to/from a scalar or JSON. The generic wrapper is what lets you reuse the same collection/array machinery for every value object rather than writing one Type subclass per combination.

Typed arrays over serialized blobs. Store array<App\Enum\Status> or collection<int> and let the type enforce the element conversion at the boundary. This keeps hydration honest — you get real objects/enums out of the column instead of array<string, mixed> and a prayer.

Keep DQL and repositories aware. A custom DBAL type does not automatically give you DQL functions. If you need WHERE column CONTAINS :x, write a custom DQL function and test it against the same type name; otherwise you'll have a type that only works on write.

Test the type in isolation, not through the ORM. This is the fastest feedback loop and the one most people skip:

$type = Type::getType('collection<App\Value\Money>');

$this->assertSame(
    [['amount' => 100, 'currency' => 'CZK']],
    $type->convertToDatabaseValue([new Money(100, 'CZK')], $platform)
);

$this->assertEquals(
    [new Money(100, 'CZK')],
    $type->convertToPHPValue('[{"amount":100,"currency":"CZK"}]', $platform)
);

Add a null case and an empty-array case to every one of these tests — they're the two that break in production.

Migration parity check. After adding a typed column, run doctrine:schema:validate and a migrations:diff. If the diff wants to change the column every time you run it, your type name isn't being persisted in the column comment, and you'll be fighting phantom migrations forever. Fix that before writing any more mappings.

Extension point: decorate, don't fork. If you need different serialization (e.g. MessagePack instead of JSON), wrap the package's type in your own class that delegates and overrides only convertTo*. Forking a 0-star package is a maintenance liability you don't need.

Gotchas and Tips

Registration timing is the #1 failure mode. If you register types in a queued job, an Artisan command, or lazily inside a repository, you'll get "Unknown column type" errors that appear only in workers or only on the second request. Register in a service provider's boot() (or register() with defer off) and guard with Type::hasType() so Octane and long-running workers don't double-register.

Type names are strings in your database schema. Doctrine stores the type name in the column comment for schema comparison. A name containing <, > and a PHP FQCN is fragile: renaming App\Value\Money silently invalidates every existing column comment, and migrations:diff will want to alter them all. Prefer short, stable inner aliases over FQCNs, and plan the rename path (usually a no-op migration that rewrites comments).

Parser edge cases. Nested generics (collection<array<int>>), whitespace after the comma, and case sensitivity are the usual breakages. Whichever way the package tokenises, add a test for collection< App\Foo > and for a nested parameter. If it can't handle nesting, don't design around it — reach for a concrete registered type instead.

DBAL 3 vs 4. If the package targets DBAL 2/3 conventions (Type::getName(), getSQLDeclaration signatures), you may hit deprecations or hard breaks on DBAL 4. Check composer.json constraints against your installed DBAL before you commit; pin the DBAL minor version in composer.json if needed.

Static analysis will not help you. PHPStan and Psalm see type: 'collection<...>' as an opaque string. You get no type narrowing on the property. Compensate with @var docblocks on the entity property and a CollectionType::MONEY constant so at least typos are caught by the IDE.

Nulls and empty collections are not the same thing. Make sure convertToDatabaseValue([]) and convertToDatabaseValue(null) produce distinguishable values, or your "has this been set?" logic will silently flip. Decide early whether empty means NULL or '[]', and be consistent in both directions.

Schema tool blindness. Because the type is registered dynamically, doctrine:schema:update and the migrator may not see the type unless your provider runs in those processes too — which it will if it's a real service provider, but won't if you registered it in a test harness or a custom kernel bootstrap. If migrations:diff throws "Unknown column type", that's your signal.

Don't use it where a relation belongs. collection<App\Entity\Comment> stored as JSON is tempting and almost always wrong: no foreign keys, no querying, no cascading deletes. The generic-type layer is for value collections; use OneToMany for entities. This is the most common way teams regret adopting a package like this.

Vendor risk is real. Zero stars, zero dependents, one maintainer. It's MIT and likely a few hundred lines — read every line, then either pin the exact version with a composer.lock commit or copy the relevant class into your own src/Doctrine namespace and drop the dependency. That's cheaper than a supply-chain surprise, and the code is small enough that this is genuinely viable.

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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle