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

Laravel Doctrine Laravel Package

brouwers/laravel-doctrine

Integrates Doctrine ORM into Laravel, swapping Eloquent for a data-mapper approach. Provides entity and metadata management, container bindings, repositories, and migration support so you can use Doctrine's mapping, DQL, and Unit of Work inside a familiar Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bridge and verify it still resolves on your stack before writing domain code:

composer require brouwers/laravel-doctrine

Because the package is archived, pin a version and test against your Laravel/PHP combination. If package discovery is not configured, add the provider manually in config/app.php:

Brouwers\LaravelDoctrine\DoctrineServiceProvider::class,

Then publish and read the configuration:

php artisan vendor:publish --provider="Brouwers\LaravelDoctrine\DoctrineServiceProvider"

config/doctrine.php is the first place to look. It is the whole integration surface: managers, DBAL connections, metadata paths/drivers, proxy settings, and caches. Compare its connection values with config/database.php; Doctrine does not automatically inherit Laravel's Eloquent connection configuration.

Confirm the container bindings before building anything:

php artisan tinker
>>> app(Doctrine\ORM\EntityManagerInterface::class);
>>> app(Doctrine\Common\Persistence\ManagerRegistry::class);

Create your first entity in the configured metadata path—typically app/Entities. Annotations are the expected style for this package generation:

namespace App\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=OrderRepository::class)
 * @ORM\Table(name="orders")
 */
class Order
{
    /** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
    private ?int $id = null;

    /** @ORM\Column(type="string", length=100) */
    private string $reference;

    /** @ORM\Column(type="datetime_immutable") */
    private \DateTimeImmutable $createdAt;

    public function __construct(string $reference)
    {
        $this->reference = $reference;
        $this->createdAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
    }
}

Use the schema tool only against a scratch database first:

php artisan doctrine:schema:validate
php artisan doctrine:schema:update --dump-sql
php artisan doctrine:schema:create

Your first real use case: inject EntityManagerInterface, persist an entity, flush, and read it back through a repository:

use Doctrine\ORM\EntityManagerInterface;

final class CreateOrder
{
    public function __construct(private EntityManagerInterface $em) {}

    public function handle(string $reference): Order
    {
        $order = new Order($reference);

        $this->em->persist($order);
        $this->em->flush();

        return $order;
    }
}

$order = $em->getRepository(Order::class)->find($id);

Once that path works end to end, you have verified mapping discovery, the DBAL connection, proxy generation, and the container binding.

Implementation Patterns

Keep EntityManager out of controllers. Wrap use cases in action or service classes that own persist()/flush(). This makes transaction boundaries explicit and prevents accidental partial writes from controller code.

Use custom repositories for query logic. Point repositoryClass at a subclass of Doctrine\ORM\EntityRepository and keep DQL there instead of scattering query strings:

final class OrderRepository extends \Doctrine\ORM\EntityRepository
{
    public function findByReference(string $reference): ?Order
    {
        return $this->findOneBy(['reference' => $reference]);
    }

    /** @return Order[] */
    public function recentWithLines(\DateTimeImmutable $since): array
    {
        return $this->createQueryBuilder('o')
            ->leftJoin('o.lines', 'l')
            ->addSelect('l')
            ->where('o.createdAt >= :since')
            ->setParameter('since', $since)
            ->getQuery()
            ->getResult();
    }
}

Use wrapInTransaction for multi-step writes. It handles rollback and rethrows:

$this->em->wrapInTransaction(function (EntityManagerInterface $em) use ($data) {
    $em->persist($order);
    $em->persist($auditEntry);
});

Do not call flush() outside that closure unless you deliberately want partial commits.

Choose DQL hydration deliberately for read endpoints. getArrayResult() avoids proxy and serialization issues for reports and JSON APIs. Only return entities from write-side actions, then map them to Laravel resources.

Use multiple entity managers for bounded contexts. The managers configuration section lets you define named managers, each with its own connection, metadata paths, and cache. Resolve them through the registry rather than injecting the default EM into every service:

$reporting = app(\Doctrine\Common\Persistence\ManagerRegistry::class)
    ->getManager('reporting');

This is useful for separating a reporting database or a legacy schema from your primary domain.

Register custom types and DQL functions at boot. If the published config exposes custom_types, mapping_types, dql functions, or subscribers, use those. Otherwise create a small service provider that decorates Doctrine\ORM\Configuration after the manager is built. Common additions: Ramsey\Uuid type, beberlei/doctrineextensions functions (DATE, GROUP_CONCAT), and timestampable/soft-delete listeners.

Prefer Doctrine lifecycle callbacks or subscribers over Laravel model events. Eloquent observers do not fire for Doctrine entities. For audit timestamps, either set values in constructors/domain methods or register a Doctrine event subscriber in config.

Use Doctrine migrations where possible. If doctrine/migrations is installed, run doctrine:migrations:diff to generate a migration, review it, then doctrine:migrations:migrate. Treat doctrine:schema:update --force as a local-development tool only.

Test against an in-memory SQLite manager. Override the default connection in phpunit.xml or a test service provider, then create the schema in setUp():

protected function setUp(): void
{
    parent::setUp();
    $this->artisan('doctrine:schema:create');
}

Clear the entity manager between tests or rebuild it per test to avoid identity-map leakage.

Batch jobs need chunked flushes and clears. In queue workers, call $em->flush() and $em->clear() every few hundred entities. Otherwise Doctrine's identity map grows until the process runs out of memory.

Cache configuration matters day to day. Use an array metadata cache locally and Redis or APCu in production. Put proxies under storage/ and clear metadata/query caches as part of deployment.

Gotchas and Tips

Archived status is the dominant risk. The last release was 2019. Expect friction with PHP 8.1+ deprecations and Laravel 9/10. The ManagerRegistry interface moved from Doctrine\Common\Persistence\ManagerRegistry to Doctrine\Persistence\ManagerRegistry in newer Doctrine ORM releases. Pin doctrine/orm to a compatible major/minor and keep a fork if you depend on this long term.

EntityManager is stateful. If bound as a singleton under Laravel Octane or in long-running queue workers, the identity map and open transaction state persist across requests/jobs. Use scoped bindings where supported, or explicitly clear() and check $em->isOpen() at job boundaries. If the EM closes after an exception, recreate it through the registry.

Doctrine has its own database connection. Laravel's DB_* environment values are not automatically passed to DBAL. Duplicate host, port, database, username, and password in config/doctrine.php, and remember that Eloquent and Doctrine transactions are not shared. Do not write to the same tables through both ORMs in one request unless you accept the consistency risk.

Proxy directories must exist and be writable. In development set auto_generate_proxy_classes to true; in production pre-generate proxies and set it to false. A missing proxy directory commonly manifests as a fatal error during lazy loading, not at boot.

doctrine:schema:update --force is destructive. It can drop columns and alter types based on mapping diffs. Always run --dump-sql first, and use migrations for any environment with data you care about.

Default naming may not match Laravel conventions. Configure an underscore naming strategy if you expect created_at rather than createdAt column names. Verify generated SQL before creating tables.

Annotations, not attributes. This package predates PHP 8 attributes. If you want attributes, you need a newer Doctrine ORM and must verify that the package's metadata driver and service provider support it. Otherwise keep annotation mapping and expect deprecation notices on newer PHP versions.

Watch for N+1 queries and lazy-loading after serialization. Doctrine proxies are not JSON-serializable and will trigger hidden queries. Use JOIN FETCH or DQL for read paths, and never return raw entities from API controllers.

clear() detaches everything. After $em->clear(), all previously loaded entities are detached. Re-fetch them or re-merge deliberately; otherwise later flush() calls silently do nothing for those objects.

Unique constraint violations are DBAL exceptions. Catch Doctrine\DBAL\Exception\UniqueConstraintViolationException and translate it into a Laravel validation error. Doctrine will not run your Laravel validation rules.

Datetime handling needs discipline. Prefer datetime_immutable columns and store UTC. Doctrine does not automatically convert timezones unless you configure it, and mixing DateTime and DateTimeImmutable in mappings causes subtle bugs.

Debugging tools are your friend. Use doctrine:info, doctrine:schema:validate, doctrine:query:dql, and doctrine:query:sql if the package registers the standard Doctrine commands. To see SQL in Laravel, attach a DBAL SQL logger through the connection configuration; Laravel Debugbar and Telescope will not automatically capture Doctrine queries unless you bridge them.

Config caching can break the provider. Avoid closures in config/doctrine.php if you run php artisan config:cache. If the package uses closures internally, test a cached configuration in staging before deploying.

Extension points are the provider and the registry. For anything the config does not expose—custom DQL functions, event subscribers, custom metadata drivers—write a small service provider that resolves the manager and mutates its Configuration or event manager. This is also where you can wrap or replace the default EntityManager binding if you need scoped lifetimes.

Low adoption means fewer answers. With 24 stars, zero dependents, and an archived repository, expect to debug integration issues from source. Read vendor/brouwers/laravel-doctrine/src early, keep the config file documented in your repository, and consider isolating the package behind repository/service abstractions so a future migration to laravel-doctrine/orm or plain Doctrine is contained.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource