javer/influxdb-odm
An InfluxDB ODM for Laravel/PHP that provides an Eloquent-style model layer for defining measurements, querying time-series data, and persisting points with less boilerplate.
Doctrine ORM in a Laravel app is a second, independent persistence layer that sits alongside Eloquent. Treat it as opt-in for domain-heavy aggregates, not a replacement for Eloquent everywhere.
Minimum steps
doctrine/persistence to ^2. This release moves to doctrine/persistence 3 and drops doctrine/annotations from the required set.composer require doctrine/orm (plus symfony/cache if you want a PSR-6 metadata cache).EntityManager singleton in a service provider — connection config, Setup::createAttributeDriver([...paths]), Configuration::setMetadataCache(), setProxyDir()/setProxyNamespace().app/Entities/ (or app/Models/Doctrine/), add config/doctrine.php with paths, proxy_dir, cache, and connection pointing at an existing Laravel connection from config/database.php.php artisan doctrine:schema:validate
# or, without the laravel-doctrine wrapper:
vendor/bin/doctrine orm:schema-tool:update --dump-sql
Where to look first
The AttributeDriver is now the canonical entry point — with AnnotationDriver decoupled in this release, attributes are the documented, default path. Read the attribute mapping reference (#[ORM\Entity], #[ORM\Column], #[ORM\ManyToOne]) and the EntityManager / UnitOfWork docs before touching repositories.
First use case
Map one entity with attributes, generate the schema diff rather than hand-writing DDL, and read it back through a repository. A single aggregate (e.g. Invoice + InvoiceLine with a real OneToMany) is the right scope: it demonstrates the relationships and cascade rules Eloquent makes easy to hand-wave.
#[ORM\Entity(repositoryClass: InvoiceRepository::class)]
#[ORM\Table(name: 'invoices')]
class Invoice
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceLine::class, cascade: ['persist', 'remove'])]
private Collection $lines;
}
Attribute mapping only. This release deliberately severs AttributeDriver from AnnotationDriver and stops requiring doctrine/annotations. If you are still on docblock annotations, add doctrine/annotations explicitly and pin the annotation driver — do not assume it is wired up. New code should use #[ORM\...] attributes exclusively, which also gets you full static analysis and IDE navigation.
Repository binding. Extend EntityRepository for aggregate roots and bind the concrete class in a service provider. Repositories should return entities or DTOs — never leak QueryBuilder to controllers. Inject the repository, not the EntityManager, into application services.
$this->app->bind(InvoiceRepository::class, fn ($app) =>
$app->make(EntityManagerInterface::class)->getRepository(Invoice::class)
);
Transaction boundaries. Use $em->wrapInTransaction(fn () => ...) at the use-case boundary, or a small Transactional middleware/service decorator. The critical rule: one transaction owner. Laravel's DB::transaction() and Doctrine's flush are separate; a rollback on one does not roll back the other.
Read vs. write split. Keep Eloquent for CRUD screens and admin panels that already exist. Reach for Doctrine when you need a rich aggregate with invariants, a value-object embeddable, or a mapping that would be awkward with Eloquent's attribute casting. Mixed usage in one request is fine as long as they are separate transactions.
Schema management. Either adopt doctrine/migrations end-to-end for Doctrine-managed tables, or generate SQL from the schema tool and paste it into Laravel migrations. Pick one; do not let both mutate the same tables.
Testing. Use in-memory SQLite with SchemaTool::createSchema() in setUp, and wrap each test in a transaction rolled back in tearDown. For anything relying on real column types (JSON, generated columns), test against the same engine as production.
Hydration for read paths. For list endpoints, use AbstractQuery::toIterable() plus custom DQL hydration (or Query::HYDRATE_ARRAY) to skip the identity map — this is where Doctrine's performance profile differs most from Eloquent's and where new developers get bitten.
The PHP 8.1 floor is a hard bump. This is not a SemVer-patch-friendly upgrade: update your CI matrix, add platform.php to composer.json ("config": { "platform": { "php": "8.1.0" } }) if your local PHP is newer than your deploy target, and check that your laravel-doctrine/orm or equivalent wrapper release supports doctrine/persistence 3 before upgrading.
doctrine/persistence 3 breaks lazy ghost proxies. Any package or app code that implemented Doctrine\Persistence\Proxy with __load()/__isInitialized() will fail. Also check custom collection or EntityManagerDecorator code.
Annotations are no longer installed for you. The upgrade may appear to "break" an existing annotation-mapped project — that is because doctrine/annotations vanished from the dependency tree. Either re-require it and configure the annotation driver explicitly, or migrate to attributes. Migrating is the long-term answer; re-requiring is the firefighting answer, and you should decide consciously which you are doing.
Do not rely on driver auto-detection. Because AttributeDriver no longer derives from AnnotationDriver, type-hinting or instanceof checks against AnnotationDriver in your own code (a surprisingly common pattern in custom EntityManager factories) will silently stop matching. Use Doctrine\ORM\Mapping\Driver\AttributeDriver directly as your type hint.
The EntityManager is stateful — reset it in long-running processes. Octane, Swoole, and queue workers will leak the identity map, the unit of work, and stale entities across jobs. Call $em->clear() between jobs (or resolve a fresh EntityManager), and never hold managed entities as properties on a singleton.
Schema tool will happily drop tables it does not know about. On a database shared with Eloquent tables, orm:schema-tool:update is destructive. Always --dump-sql first, and use --filter or a dedicated connection so Doctrine only ever sees its own tables.
Detached entities after clear() or serialization. Passing an entity through a queue, cache, or the session gives you a detached instance; persist() is a no-op and flush() will not save changes. Re-fetch by ID, or use merge() deliberately — it copies, so the original stays detached.
Lazy loading outside the request. LazyLoadingViolationException on serialization, or a proxy that fails to initialise after the entity is closed, usually means proxy directory permissions or a missing setProxyDir(). Proxies must be writable and generated before production; run orm:generate-proxies in your deploy script.
Cache configuration differs per environment. ArrayAdapter for metadata in dev (so attribute changes are picked up immediately), a real PSR-6 cache in production. The classic symptom of misconfiguration is "I changed the entity and the schema tool still reports no changes" — clear the metadata cache, not just Laravel's.
Debugging aids worth wiring up. $em->getUnitOfWork()->getScheduledEntityInsertions() / ...Updates() tells you exactly what flush will emit; a Middleware-based SQL logger (or Query::getSQL() on a suspect query) is faster than guessing. For unexpected schema diffs, orm:info and orm:mapping:describe <Entity> show what mapping Doctrine actually loaded — usually more revealing than the docs.
How can I help you explore Laravel packages today?