laravel-doctrine/fluent
Laravel Doctrine Fluent provides a fluent PHP mapping driver for Doctrine ORM entities, letting you configure entity mappings with a clean, expressive API instead of annotations, XML, or YAML files.
laravel-doctrine/fluent is a metadata driver for Doctrine ORM. Instead of describing an entity with annotations or PHP attributes, you write a small, explicit PHP class that tells Doctrine how the entity maps to the database. Your entity stays a plain POPO; the mapping becomes testable, refactorable code.
Reach for it when you want mapping in one place, want to keep domain classes free of persistence concerns, or you're porting a legacy app where annotations are no longer an option (PHP 8 attributes are fine, but fluent scales better for large, convention-heavy schemas).
Minimal setup:
composer require laravel-doctrine/orm laravel-doctrine/fluent — fluent is a companion to the ORM package, not a replacement for it.php artisan vendor:publish --provider="LaravelDoctrine\ORM\DoctrineServiceProvider") and, in config/doctrine.php, set the manager's metadata driver (meta) to LaravelDoctrine\Fluent\FluentDriver. Confirm the exact key/shape against the README for your installed version — the driver name, not the config path, is what matters.App\Mappings), or register them explicitly.FluentMap subclass per entity:namespace App\Mappings;
use App\Entities\User;
use LaravelDoctrine\Fluent\FluentMap;
class UserMap extends FluentMap
{
public function map(): void
{
$this->className(User::class);
$this->table('users');
$this->id('id')->type('integer')->generatedValue();
$this->string('name')->length(120);
$this->string('email')->unique();
$this->boolean('active')->default(true);
$this->datetime('createdAt')->columnName('created_at');
}
}
First use case: port exactly one existing entity (a User is ideal) and validate it with php artisan doctrine:schema:validate and php artisan doctrine:migrations:diff. If the generated SQL matches the existing table, the driver is wired correctly and you can migrate the rest of the schema entity by entity.
One map per entity, mirroring the entity directory. User → UserMap, Order → OrderMap. Keeping the naming predictable is what lets the driver discover maps from a namespace/path instead of you maintaining a registry list.
Fields are chained builders. Every helper (string, integer, boolean, text, decimal, datetime, date, json, array) returns a builder you can decorate: ->nullable(), ->unique(), ->default(...), ->length(...), ->columnName(...), ->precision()/->scale(), ->options([...]) for platform-specific column flags. Read top to bottom, the map is the schema.
Relations use the same fluent style:
$this->manyToOne('group', Group::class)
->inversedBy('users')
->joinColumn('group_id')
->nullable();
$this->oneToMany('posts', Post::class)
->mappedBy('author')
->cascadePersist();
$this->manyToMany('roles', Role::class)
->inversedBy('users')
->joinTable('user_role');
Prefer fully-qualified class names for targets — short-name/alias resolution is a source of "entity not found" errors when maps move between namespaces.
Extract a shared base map instead of copy-pasting. Nearly every entity repeats an id, plus timestamps or soft-delete columns. A BaseMap extends FluentMap (or a trait per concern) with a mapDefaults() hook keeps maps short and makes a schema-wide change (e.g. switching to UUIDs) a single edit.
Other things the builder covers: embeddables ($this->embedded('address', Address::class)), single-table inheritance with discriminators, lifecycle callbacks/entity listeners, and table-level indexes and unique constraints. Treat anything you'd otherwise write in an attribute as expressible here.
Workflow: edit the map → clear the metadata cache → doctrine:migrations:diff → review the SQL → doctrine:migrations:migrate. Because maps are plain classes, you can also assert on them in a unit test (instantiate the map, run it, inspect the resulting metadata) without touching a database.
config/doctrine.php still says attributes/annotations, Doctrine will happily map nothing from your map classes and you'll get "entity has no mapped fields" or a stale schema. This is the first thing to check when a new map "does nothing."php artisan doctrine:clear-metadata-cache). Local/staging with APP_DEBUG on usually caches to array and hides this; production will bite you.columnName() explicitly or configure the naming strategy — otherwise diffs will propose renaming every column.Type::addType(), Type::getName(), and the old fetchAll()/execute() connection API. If you register custom types through custom_types in the Doctrine config or a service provider, audit each type: register by name (Type::getTypeRegistry()->register()) and drop getName(). Any code touching ClassMetadataInfo should move to ClassMetadata.laravel-doctrine/* set in lockstep (orm, migrations, extensions, fluent). A split major between orm and fluent is the usual cause of driver/class-not-found errors after a composer update.composer.json on Packagist. Note also that the release note lists only additions even though this is a major bump, so read the full diff or composer why-not before upgrading.FluentMap for shared behaviour, wrap repeated field groups in traits, and swap/extend the driver class itself if you need custom discovery. There is no need to fork the package for conventions — the base map is the intended seam.doctrine:migrations:diff is your fastest feedback loop on a map change and catches the ordering/uniqueness mistakes that are otherwise invisible until deployment.How can I help you explore Laravel packages today?