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 Bundle Laravel Package

doctrine/doctrine-bundle

Doctrine DBAL & ORM Bundle for Symfony, integrating the powerful Doctrine database abstraction layer and object relational mapper into Symfony applications with flexible configuration for entities, connections, and schema management.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and let Flex do the wiring:

composer require doctrine/doctrine-bundle
# requires doctrine/orm (2.x or 3.x) — Flex recipe writes config/packages/doctrine.yaml

The recipe creates config/packages/doctrine.yaml and sets DATABASE_URL in .env. On a fresh Symfony app you normally need three edits:

# config/packages/doctrine.yaml
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
    orm:
        auto_generate_proxy_classes: true
        mappings:
            App:
                type: attribute
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

when@prod:
    doctrine:
        orm:
            auto_generate_proxy_classes: false   # proxies are pre-generated by cache:warmup
            query_cache_driver: { type: pool, pool: doctrine.system_cache_pool }
            metadata_cache_driver: { type: pool, pool: doctrine.system_cache_pool }

Where to look first. The authoritative config reference is on symfony.com (reference/configuration/doctrine.html); the bundle's own docs/ folder is thin by comparison. Everything this bundle does is wiring — it turns YAML into DBAL Connections and ORM EntityManagers and registers the console commands. When something is wrong, you're usually debugging DBAL/ORM itself, not the bundle.

First real use case. Create an entity, then run the read-only commands before you touch the schema:

bin/console doctrine:mapping:info          # is my entity actually mapped?
bin/console doctrine:schema:validate       # mapping + sync check against the DB
bin/console doctrine:query:sql 'SELECT 1'  # does the connection work?
bin/console doctrine:query:dql 'SELECT e FROM App\Entity\Post e' --limit=5

If mapping:info doesn't list your class, the problem is mapping config, not your code. Don't reach for doctrine:schema:update in any environment you care about — put doctrine/doctrine-migrations-bundle next to this one and use doctrine:migrations:diff / migrate.

Implementation Patterns

Inject the manager, not the registry. EntityManagerInterface is autowired to the default manager; ManagerRegistry is only worth it when you genuinely switch managers at runtime.

final class PostRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Post::class);
    }
}

Classes extending ServiceEntityRepository/EntityRepository are auto-registered as services via the doctrine.repository_service tag — you can inject them directly into controllers and handlers without any services.yaml entry.

Multi-connection / multi-manager setups. Declare explicit connections and entity_managers, each manager with its own mappings:, and type-hint the per-manager alias (doctrine.orm.<name>_entity_manager) rather than the generic interface. The generic EntityManagerInterface alias only exists for the default manager; with two managers, autowiring it fails loudly (which is the good outcome).

Read/write splitting. Configure replica under a connection and primary for writes; the bundle routes reads to replicas and lets you force the primary with $conn->executeQuery($sql, [], [], ['primary' => true]) (or $em->getConnection() at the DBAL level). Do not rely on replicas for read-your-writes — after a flush() go to the primary.

Custom DBAL behaviour belongs in a middleware. Tag with doctrine.middleware (optionally priority:) and implement Doctrine\DBAL\Driver\Middleware. This is the supported place for query logging, tracing, tenant switching, and slow-query instrumentation. The bundle ships its own logging/profiling middleware and a built-in priority ordering, so pick a priority deliberately if order matters.

Event hooks — use the attributes.

#[AsDoctrineListener(event: Events::postFlush, connection: 'default')]
#[AsEntityListener(event: Events::prePersist, method: 'prePersist', entity: Post::class)]

The tags (doctrine.event_listener, doctrine.event_subscriber, doctrine.orm.entity_listener) still work if you prefer YAML; the attributes just save boilerplate. Note AsDoctrineListener attaches to a connection, not an entity manager.

Extension points you'll actually touch:

  • Custom DQL functions → doctrine.orm.dql.string_functions / numeric_functions / datetime_functions.
  • Custom column types → implement Type and register under doctrine.dbal.types (or the doctrine.dbal.type tag for a DI-managed type).
  • Custom TypedFieldMapper for value objects → doctrine.orm.typed_field_mapper.
  • Per-entity second-level cache → #[Cache(usage: CacheUsage::READ_ONLY, region: 'posts')] plus orm.second_level_cache.regions.

Async work. doctrine://default is a Messenger transport, and DoctrineTransport gives you the outbox pattern via messenger.transport.doctrine config — enable it per-transport, not globally, since it changes delivery semantics (rows are claimed with FOR UPDATE).

Testing. Set when@test: doctrine.dbal.dbname_suffix: '_test', then wrap tests with dama/doctrine-test-bundle so every test runs in a rolled-back transaction. Without it, $em->clear() and fixture reloads make suites slow and order-dependent.

Gotchas and Tips

A closed entity manager poisons everything after it. Any exception inside a flush (constraint violation, deadlock) closes the EM; subsequent calls throw The EntityManager is closed. Catch, call $registry->resetManager() (or ManagerRegistry::resetManager($name)), and retry — don't keep using the same instance.

flush() in loops. The Unit of Work grows forever. Batch with $em->flush(); $em->clear(); on every N records — and remember clear() detaches everything, so re-fetch or re-merge() entities you still hold references to. For read-only exports, $em->getConnection()->iterateAssociative() skips the ORM entirely and is dramatically faster.

Doctrine is not your transaction manager for side effects. flush() inside a transaction still doesn't roll back sent emails, dispatched HTTP calls, or pushed messages. Use $em->wrapInTransaction(fn () => ...) and dispatch after commit (PostFlushEventArgs, or the outbox transport above).

Cache config is the #1 production incident source. metadata_cache_driver/query_cache_driver set to an array driver in prod means re-parsing annotations/attributes on every request; set to a shared pool means stale schema after deploy unless cache:clear runs as part of the release. The modern form is the doctrine.system_cache_pool (metadata/query) and doctrine.result_cache_pool (result) with type: pool. Also note auto_generate_proxy_classes: true in prod writes to var/cache at runtime and is a silent performance and permissions bug — keep it false and let cache:warmup generate proxies.

auto_mapping and lazy-object toggles are deprecated. doctrine.orm.auto_mapping (the bundle-scanning convenience) is deprecated in favour of explicit mappings:, and both enable_lazy_ghost_objects (3.1) and enable_native_lazy_objects are on their way out because ORM 3 always uses lazy ghost objects. Same for doctrine.orm.controller_resolver.auto_mapping (2.12/3.1) — if your config still has it, it's dead weight and emits deprecations. report_fields_where_declared matters for ORM 3 field inheritance; the bundle enables it for you on ORM 3, but hand-rolled EntityManager setups elsewhere won't.

Profiler overhead is real. The logging/profiler middleware is invaluable in dev (query count, duplicate queries, EXPLAIN in the Doctrine panel) and costs measurable time under load. Check doctrine.dbal.logging and profiling are off in prod.

server_version and default_dbname quirks. Without server_version, DBAL probes the server on connect — that's fine but adds a round trip; set it explicitly in CI. default_dbname (for database:create/drop) has had multiple fixes across 2.11–2.12 for Postgres and MySQL; if you're on an older 2.x, pin ≥2.12.1. Use dbname_suffix per environment rather than string-concatenating in DATABASE_URL.

Schema filters save multi-tenant setups. dbal.schema_filter (a regex) keeps migrations:diff and schema:validate from trying to drop the tables your tenant logic owns. Get this wrong and migrations:diff cheerfully writes DROP TABLE.

Second-level cache specifics. SLC only helps find()/getReference() and to-one associations; it does not cache DQL result sets or collections unless you mark them. If you pin below 3.2.3, region_lifetime is ignored for regions you declare explicitly (fixed in 3.2.3) — a nasty silent config no-op.

Debugging aids, in order of usefulness:

  1. doctrine:schema:validate --skip-sync — mapping errors only.
  2. doctrine:mapping:info — what the driver actually discovered.
  3. The profiler's Doctrine panel — query count, params, and the exact SQL with the DQL that produced it.
  4. doctrine:query:dql with --hydrate=array to isolate whether a hydration issue or a query issue is biting you.
  5. The 3.2 line adds console commands for inspecting which listeners/subscribers are actually wired to a connection — reach for those instead of var_dump-ing the event dispatcher when an event "isn't firing".
  6. Every doctrine:* command takes --em= and --connection=; forgetting this when you have two managers produces confusing "table not found" errors.

Version matrix. 2.18.x is the maintenance line for ORM 2.x-era stacks; 3.x tracks ORM 3 and supports Symfony 8 (added in 3.1). Choose 3.x for new projects; the deprecation noise on 2.x is now substantial, and the SLC, default_dbname, and proxy-cache bugs you're most likely to hit are only fixed there.

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.
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
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo