symfony/doctrine-bridge
Symfony Doctrine Bridge connects the Doctrine project with Symfony components, supplying form types, validators, security user providers, event listeners, and other integration code for Doctrine-backed Symfony apps. A core part of the Symfony framework.
symfony/doctrine-bridge is not a library you adopt; it is the glue layer that lets Doctrine talk to Symfony's Validator, Form, Security, Messenger, Serializer, PropertyInfo, Lock, Cache, and Session components. As a Laravel developer the first question is not "how do I install this?" but "why is it in my composer.lock at all?" — run:
composer why symfony/doctrine-bridge
In a normal Laravel app it shows up only when something dragged Doctrine in (usually doctrine/doctrine-bundle / doctrine/orm, or a package that depends on Symfony Form or Security with Doctrine integration). You almost never composer require symfony/doctrine-bridge directly: its version is pinned to the Symfony minor (8.1.x with Symfony 8.1, 7.4.x with 7.4), and DoctrineBundle is the thing that actually wires the services.
Where to look first, in order of day-to-day usefulness:
Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity + UniqueEntityValidator — the only class most people touch directly.Symfony\Bridge\Doctrine\Security\User\EntityUserProvider — Doctrine-backed auth.Symfony\Bridge\Doctrine\Messenger\* — doctrine:// queue transport and its middleware.Symfony\Bridge\Doctrine\Form\Type\EntityType and DoctrineOrmTypeGuesser — Symfony Forms only.Symfony\Bridge\Doctrine\Types\* — UuidType, UlidType, DayPointType, TimePointType.Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor — metadata for Serializer/Validator.Symfony\Bridge\Doctrine\ManagerRegistry / ContainerRepositoryFactory — repositories as services.Symfony\Bridge\Doctrine\Middleware, SchemaListener, CacheWarmer\ProxyCacheWarmer — infrastructure.First real use case (works in any framework, including Laravel, once you have an EntityManagerInterface):
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Validation;
#[UniqueEntity(fields: ['email'], message: 'This email is already registered.')]
class User
{
public function __construct(
public string $email,
) {}
}
$validator = Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator();
// Requires the UniqueEntityValidator to be registered with a ManagerRegistry,
// which DoctrineBundle does for you in Symfony; in Laravel you would wire it manually.
If you are not already running Doctrine ORM, that snippet is the point at which you should stop and ask whether Eloquent's unique:users,email rule is enough. Usually it is. The bridge only pays off when you have committed to Doctrine and to Symfony components.
Validation: UniqueEntity over a Doctrine repository. This is the bridge's most portable feature. It supports composite keys, ignoreNull, errorPath, and a custom repositoryMethod returning a scalar count:
#[UniqueEntity(
fields: ['tenant', 'slug'],
ignoreNull: true,
repositoryMethod: 'countByTenantAndSlug',
errorPath: 'slug',
)]
class Page {}
Keep repositoryMethod cheap and index-backed; it runs on every validate() call. With Uuid/Ulid identifier objects, upgrade to at least 7.2.7/7.3 — earlier versions compared Stringable IDs incorrectly.
Security: EntityUserProvider. Wire it explicitly when you are not on the full Symfony security bundle:
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;
$provider = new EntityUserProvider($managerRegistry, User::class, 'email');
$user = $provider->loadUserByIdentifier('[email protected]');
Property-based loading is fine until you need case-insensitive or tenant-scoped lookups; then implement Symfony\Component\Security\Core\User\UserLoaderInterface and hand it to the provider. Note the 7.3.4 fix making providers pass request attributes to the loader — custom loaders that read attributes need that patch.
Messenger: Doctrine as a queue. Instead of Redis/SQS you can keep jobs in the same database:
framework:
messenger:
transports:
async:
dsn: 'doctrine://default?table_name=messenger_messages&queue_name=default&redeliver_timeout=3600&auto_setup=true'
Pair it with the bridge middleware, which is where most correctness lives:
framework:
messenger:
buses:
messenger.bus.default:
middleware:
- doctrine_ping_connection # reconnect after idle
- doctrine_transaction # wrap handler in a DB transaction
- doctrine_close_connection # optional: release connection after handling
In Laravel terms this is the closest analogue to database queue driver, but with real transaction semantics around the handler. auto_setup: true creates the table on first use — convenient locally, a footgun on databases with restricted DDL permissions.
Infrastructure without ORM. Several bridge classes need only DBAL, which makes them usable in Laravel against an existing connection:
Symfony\Bridge\Doctrine\Lock\DoctrineDbalStore (DSN doctrine://default) for cross-process locks; DoctrineDbalPostgreSqlStore gives advisory locks.Symfony\Bridge\Doctrine\Cache\DoctrineDbalAdapter for a shared cache table.Symfony\Bridge\Doctrine\HttpFoundation\DoctrineDbalSessionHandler for sessions.These are genuinely useful if you already have a DBAL Connection and want Symfony-component behaviour without adding Redis.
Repositories as services. ContainerRepositoryFactory lets Doctrine instantiate repositories from the DI container, so constructor injection works:
class InvoiceRepository extends EntityRepository
{
public function __construct(ManagerRegistry $registry, private ClockInterface $clock)
{
parent::__construct($registry, Invoice::class);
}
}
Register with doctrine.orm.entity_manager + doctrine.repository_service tags. Without this, repositories are new-ed by Doctrine and silently lose dependencies.
Custom Doctrine types. Register UuidType/UlidType (or the newer DayPointType/TimePointType) via doctrine.dbal.types. For Uid types, the entity loader also needs the matching fix (7.3.10/7.4.4): without it, find() with a Uuid argument fails to convert.
Long-running workers. Symfony's IdleConnectionListener only runs on the master request (fixed in 7.2.9/7.3.2) and now closes all expired connections. In queue workers, still call $em->clear() after each job and rely on doctrine_ping_connection rather than assuming the connection survived.
Schema tooling. schema_filter is honoured by schema listeners from 7.4.6 onward. If you use schema_filter to hide tables owned by another app, older versions will happily emit DROP TABLE for them in doctrine:migrations:diff.
require the bridge directly in a Laravel project. Let doctrine/doctrine-bundle constrain it. A manual requirement drifts out of sync with the Symfony minor and manifests as bizarre "method does not exist" errors inside DoctrineBundle.TableEditor migration, DBAL 4.5 restore, <4.5 fixes). Mixing bridge 8.1 with DBAL 3.x is not supported; run composer update --with-all-dependencies doctrine/dbal together with Symfony.ProxyCacheWarmer now filters non-.php files (7.4.15/8.1.2), which specifically fixed cache:clear dying when editors drop .gitkeep, .DS_Store, or backups into the proxy dir. Still: keep the proxy dir writable, set auto_generate_proxy_classes: false in production, and run doctrine:ensure-production-settings in CI.lazy: true, pin to those patch levels; debugging symptoms look like "my subscriber randomly doesn't fire".#[ORM\Column] on the child is being skipped, this is the cause.sameDatabaseChecker temporary tables. Historically fragile: Oracle reserved-word rename, PDO path, catching driver exceptions, "session table" detection. If doctrine:query:sql or lock/session setup fails oddly on Oracle or PDO, check that patch level before writing workarounds.UniqueEntity with Stringable IDs, composite keys, and ignoreNull — each has had a regression. Validate with an integration test that hits the real repository, not a mocked one; the validator's behaviour depends on Doctrine metadata and the actual getResult() shape.AbstractDoctrineExtension (custom bundles extending it must move to AbstractBundle/PrependExtensionInterface), auto-mapping entities to controller arguments (declare EntityValueResolver mapping or use explicit #[MapEntity]), and generic constraint option handling. If you are upgrading from 7.x, grep for these before the bump.bin/console doctrine:query:sql for sanity checks, --debug for verbose listener registration, and EntityManager::getUnitOfWork()->getScheduledEntityInsertions() when changes vanish. For "why is this proxy uninitialized", $em->getUnitOfWork()->isInIdentityMap($entity) and Doctrine\ORM\Tools\Debug style checks beat var_dump.ManagerRegistry, custom RepositoryFactory, doctrine.event_listener / doctrine.event_subscriber tags, and (ORM side) #[AsEntityListener]. Most "the bridge won't let me…" problems are solved by registering one of these rather than patching the bridge.UniqueEntity, DBAL lock/cache/session stores, Messenger doctrine:// transport) are worth reaching for only once Doctrine is already in the stack. If Eloquent is your ORM, the bridge's surface area relevant to you is approximately zero — and that is the correct assessment, not a criticism.How can I help you explore Laravel packages today?