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

Orm Laravel Package

doctrine/orm

Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence for PHP objects on top of the DBAL, with DQL, an object-oriented SQL dialect inspired by Hibernate's HQL.

View on GitHub
Deep Wiki
Context7

Getting Started

Doctrine ORM is not Laravel's default persistence layer — Eloquent is. So "getting started" means deciding where Doctrine earns its place in a Laravel codebase, then wiring it in without fighting the framework.

Minimal install path

composer require doctrine/orm doctrine/dbal
# Laravel bridge (service provider, facades, config, Artisan commands)
composer require laravel-doctrine/orm
# If you want Doctrine-owned schema
composer require doctrine/migrations laravel-doctrine/migrations
php artisan vendor:publish --tag="doctrine"

Then in config/doctrine.php:

  • Point managers.default.connection at a Laravel DB connection (mysql, pgsql, etc.) — the bridge reuses Laravel's database.php credentials, so you don't duplicate DSNs.
  • Set managers.default.paths to your entity directories (e.g. app/Domain/**/Entity).
  • Ensure proxy_dir (storage/framework/doctrine/proxies or similar) is writable and gitignored.
  • Use attribute mapping (#[ORM\Entity]), not annotations — annotations were removed in 3.x and the ecosystem is attribute-only now.

Where to look first

  1. The stable docs index: Working with Objects (Unit of Work, persist/flush, identity map) — this is the mental model difference from Eloquent.
  2. DQL and The QueryBuilder — the actual day-to-day surface.
  3. Configuration reference for the metadata/query caches and proxy settings.
  4. The laravel-doctrine/orm README for the Artisan commands (doctrine:schema:validate, doctrine:query:dql, etc.).

First use case

Pick a read-heavy, well-modelled slice — a reporting query, an import pipeline, or a domain aggregate with real invariants — and leave the rest on Eloquent. A good first win: replace a sprawling Eloquent query with a typed repository method backed by DQL that returns entities or scalar arrays, so you can compare hydration behaviour side by side without committing the whole app.

#[ORM\Entity(repositoryClass: InvoiceRepository::class)]
#[ORM\Table(name: 'invoices')]
class Invoice
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
    private int $id;

    #[ORM\Column(type: 'string', length: 36, unique: true)]
    private string $number;

    #[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'invoices')]
    private Customer $customer;
}
$invoice = new Invoice($number, $customer);
$em->persist($invoice);
$em->flush();          // no save(); the flush is the commit

Implementation Patterns

Unit of Work, not ActiveRecord. The core discipline: mutate objects, persist() new ones, flush() once at the end of the unit of work. Doctrine diffs the object graph and emits the minimal SQL. Resist per-row flushes.

$em->wrapInTransaction(function (EntityManagerInterface $em) use ($order) {
    $order->markPaid();
    $em->persist(new Payment($order, $amount));
    // single flush at commit
});

wrapInTransaction() is the idiomatic replacement for manually managing beginTransaction/commit/rollback/close.

Repositories as the query boundary. Register a repository class on the entity and let controllers/services depend on it — not on the EntityManager.

class InvoiceRepository extends EntityRepository
{
    /** @return Invoice[] */
    public function overdueSince(DateTimeImmutable $cutoff): array
    {
        return $this->createQueryBuilder('i')
            ->where('i.dueDate < :cutoff')
            ->andWhere('i.paidAt IS NULL')
            ->setParameter('cutoff', $cutoff)
            ->orderBy('i.dueDate', 'ASC')
            ->getQuery()
            ->getResult();
    }
}

Prefer the Criteria API for composable filters ($repo->matching($criteria)) so callers can add andWhere/orderBy without duplicating repository methods.

DQL for joins, QueryBuilder for conditionals. DQL is expressive and portable across drivers; the QueryBuilder is what you want when the WHERE clause is assembled dynamically.

$dql = 'SELECT c, COUNT(i.id) AS invoiceCount
        FROM App\Domain\Customer\Entity\Customer c
        LEFT JOIN c.invoices i
        GROUP BY c.id
        HAVING COUNT(i.id) > :threshold';

$rows = $em->createQuery($dql)
    ->setParameter('threshold', 5)
    ->getArrayResult();   // scalar/array hydration for reports

Hydration modes are the performance dial. getResult() (objects) for domain work; getArrayResult() / HYDRATE_SCALAR for grids, exports and API payloads where you don't need managed entities. Partial hydration (SELECT PARTIAL i.{id,number} ...) buys you a little, but beware uninitialized fields.

Pagination. Doctrine\ORM\Tools\Pagination\Paginator handles the COUNT query and fetch-join correctness for you; combine with Laravel's LengthAwarePaginator if you want the familiar response shape.

Streaming large result sets. Use toIterable() with batched clear() rather than getResult() for imports.

foreach ($em->createQuery('SELECT p FROM Product p')->toIterable() as $i => $product) {
    $product->reindex();
    if ($i % 500 === 0) {
        $em->flush();
        $em->clear();   // detaches everything — see gotchas
    }
}

Value objects via embeddables and custom types. #[ORM\Embedded] for Money, Address; a DBAL\Types\Type subclass for Uuid/Email/enum-like columns. This is where Doctrine beats Eloquent decisively for domain modelling.

Timestamps and auditing via lifecycle events. prePersist/preUpdate for createdAt/updatedAt, or an event subscriber for a full audit trail. #[ORM\HasLifecycleCallbacks] plus methods is fine for small concerns; subscribers scale better.

Inheritance when the model demands it. Single-table inheritance (#[ORM\InheritanceType('SINGLE_TABLE')] + discriminator) for simple polymorphic taxonomies; joined inheritance only when column sparsity justifies the join cost.

Coexistence with Eloquent. Keep them on separate tables. Eloquent remains fine for session/auth/breeze scaffolding; Doctrine owns the domain aggregate. Do not write to the same table through both — the identity maps and caches will disagree silently.

Schema ownership. Either Doctrine Migrations owns the schema (and you drop Laravel migrations for those tables) or Laravel does (and you disable doctrine:schema:update outside local). Mixing both produces diff noise and lost migrations; pick one owner per table and document it.

Testing. SQLite :memory: for fast unit tests, transactions wrapped around each test, and $em->clear() in tearDown(). For Postgres-specific DQL, run a separate integration suite against the real driver — behavioural differences will bite you otherwise.

Laravel integration tips. Bind EntityManagerInterface in a service provider, resolve repositories through a small registry or getRepository(), and keep controllers free of flush(). Add Artisan shortcuts for doctrine:schema:validate in CI and doctrine:query:dql for ad-hoc debugging.

Gotchas and Tips

  • No save(). Forgetting flush() is the #1 beginner error; so is calling it inside a loop. One flush per unit of work.
  • The identity map is a cache you didn't ask for. $em->find(Invoice::class, 1) twice returns the same instance. If another process changed the row, you'll see stale data until $em->refresh($entity) or $em->clear(). Long-running workers and queue jobs need an explicit clear() (or a fresh manager) between jobs.
  • Closed managers after failed flushes. A failed flush() may close the EntityManager; every subsequent operation throws. With the ManagerRegistry, call resetManager() and rebuild — don't try to keep using the dead one.
  • Octane and stateful managers. The EM is request-scoped state. Under Octane, reset the manager per request (or at least per worker iteration) or you'll leak entities across requests.
  • Lazy loading is N+1 in disguise. $invoice->getCustomer()->getName() inside a loop fires a query each time. Fix with a JOIN ... ADD SELECT fetch join in DQL, or a repository method that eager-fetches. Avoid setting fetch: 'EAGER' globally — it just moves the N+1.
  • Proxies are not your entity. $customer from getReference() is an uninitialised proxy; instanceof works, get_class() does not return your class. Serialising entities directly (queues, JSON responses) triggers proxy initialisation and can dump the whole graph — use DTOs or HYDRATE_ARRAY.
  • clear() detaches everything. After clear(), previously loaded entities are unmanaged; mutating and flushing them silently does nothing. Re-fetch or re-merge(). This is the classic bug in batch-import loops that hold a reference to a "parent" entity across clear() calls.
  • Cascade configuration is opt-in. persist does not cascade by default. cascade: ['persist', 'remove'] on the owning side, orphanRemoval: true when removing a child should delete it. Getting this backwards is how you get FK violations or orphan rows.
  • Metadata cache in development. A production metadata cache (ApcuCache, RedisCache, file cache) will serve stale mappings after you edit attributes. Use ArrayAdapter (or null) locally; warm the cache in deploy. The same applies to the query cache.
  • DQL is not SQL. No SELECT *, every root needs an alias, LIMIT isn't DQL (use setMaxResults/setFirstResult), HAVING requires the aggregate in SELECT in some drivers, and ORDER BY on a non-selected field needs it in the select list for some hydration modes.
  • Parameter binding. Always use named parameters — DQL string interpolation is an injection vector and breaks the query cache.
  • Doctrine\DBAL\Types and dates. Use datetime_immutable / date_immutable with DateTimeImmutable; mutable date types are a footgun in entities with setters.
  • Schema diff false positives. Doctrine's schema comparator frequently proposes changes for collations, unsigned integers, enums and Postgres-specific defaults. Tune with schema_filter / a custom schema manager rather than accepting the diff blindly. Never run doctrine:schema:update --force in production.
  • Laravel migrations still run. Don't let both migration systems race in CI; gate one behind an env flag or a separate command namespace.
  • Debugging slow queries. Enable DBAL logging/middleware in local (doctrine.dbal.logging), watch the query log, and use doctrine:query:dql to iterate on DQL without touching code. $query->getSQL() shows the generated SQL.
  • Extension points worth knowing. Custom DBAL types, event subscribers (postLoad, preFlush, onClear), custom hydrators, query hints (->setHint(Query::HINT_REFRESH, true)), second-level cache for reference data, and NativeQuery + ResultSetMapping when you must drop to SQL but want entity results.
  • Version pinning. 2.x → 3.x removed annotations and tightened types; 4.0 is in development. The laravel-doctrine/orm bridge's supported ORM range constrains you — check compatibility before upgrading either side, and read the UPGRADE guide before jumping minors.
  • getReference() vs find(). Use getReference() when you only need the FK for an association (avoids a SELECT); use find() when you need the data. Passing a full entity you already have is cheaper than either.
  • Serialisation and DTOs. Doctrine entities are not DTOs. Map to arrays or dedicated response objects at the boundary; it sidesteps proxy serialisation, lazy-load storms and accidental circular references.
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.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager