hakam/multi-tenancy-bundle
Hakam Multi-Tenancy Bundle is a Laravel package for adding multi-tenant support with tenant-aware database connections, migrations, and configuration, helping isolate tenant data and simplify managing multiple tenants in one application.
First, reset expectations: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/app.php entry, no Eloquent. Map your Laravel instincts accordingly:
| Laravel habit | Symfony equivalent here |
|---|---|
| Middleware resolving tenant | Kernel event listener on kernel.request |
| Eloquent global scope | Doctrine SQLFilter / entity listener |
config/tenancy.php |
config/packages/*.yaml + bundle Extension |
| Service provider bindings | services.yaml + compiler passes |
Minimal path to a working tenant-aware app:
composer require hakam/multi-tenancy-bundle — v3.1.0 requires PHP 8.1+. If you are still on 8.0, you are pinned to the v3.0.0 line and get no fixes at all, so check platform.php in composer.json before you start.config/bundles.php:
Hakam\MultiTenancyBundle\HakamMultiTenancyBundle::class => ['all' => true],
config/packages/. Multi-tenancy bundles generally need three decisions up front: how a tenant is identified (domain, subdomain, header, or path), where tenant metadata lives (a tenant table or a static map), and how data is isolated (shared schema with a tenant_id, or database-per-tenant).acme.example.com resolves tenant acme, Doctrine filters every query by tenant_id, and a debug endpoint dumps the resolved tenant. This exercises resolution, context propagation, and query scoping in one pass — the three things that break independently later.Where to look first: the bundle's DependencyInjection/ directory (what config keys actually exist), then its event listeners (resolution order), then its Doctrine integration. With 133 stars and zero dependents, the source is the documentation for edge cases. v3.1.0 exists mostly to repair a broken documentation link, so treat the in-repo source as more reliable than the rendered docs regardless.
Resolution at the edge, propagation in the middle. Resolve the tenant once in a kernel.request listener and store it in a request-scoped context service. Everything downstream — repositories, mailers, file storage, cache — reads from that service rather than re-parsing the host. Never let controllers resolve tenants themselves; you will end up with two code paths that disagree.
Doctrine filtering for shared-schema tenancy. Enable the bundle's filter and make sure it is active for every entity manager you use, not just the default one. Wrap writes in an entity listener that stamps tenant_id on persist so a forgotten assignment cannot create an orphan row. Treat the filter as a safety net, not a substitute for explicit tenant_id in queries — filters can be disabled at runtime and silently widen a result set.
Database-per-tenant switching. If you go this route, the tenant context must mutate the connection parameters before the first query, and you must clear() the entity manager when switching. Long-running processes (Messenger workers, Swoole/RoadRunner) are where this pattern earns its complexity — and where it bites hardest.
Console and async are separate worlds. There is no request in a command, so no resolver runs. Every console command that touches tenant data needs an explicit --tenant= option that pushes the context manually. For Messenger, stamp the tenant identifier into the message envelope (a custom Stamp) and restore context in a middleware — otherwise a worker will happily process tenant A's job against tenant B's connection.
Testing. Functional tests should set the Host header rather than mocking the context service; that validates the resolver, which is the part most likely to be misconfigured. For shared-schema, load fixtures inside an explicit tenant context and assert that a query for tenant A returns zero rows from tenant B — make that a standing regression test.
Integration tips. Namespace cache keys, session cookie domains, uploaded-file paths, and mailer sender addresses by tenant — the bundle will not do this for you, and cross-tenant cache bleed is the classic post-launch incident. Subdomain tenancy also means cookies need domain: .example.com with tenant-scoped session names, or users will share sessions across tenants.
Upgrade cadence. v3.x is a stable API as of 3.1.0 — the 3.0.0 → 3.1.0 delta is a docs link fix plus the PHP 8.1 floor, no signature or config changes. That means a minor bump is safe to take, but do not read a quiet changelog as "mature": there are still zero dependents, so regressions land on you first.
composer.json, read the changelog before every bump, and be prepared to fork. The listed release date (2026-04-26) is ahead of the current date — verify what you actually install against Packagist before trusting the metadata.composer update will quietly resolve you back to v3.0.0 and you will spend an afternoon wondering why the docs link 404s.$em->clear() on every context switch, or you will serve a stale object across tenants.for each tenant: doctrine:migrations:migrate --em=tenant) and make it idempotent, or you will drift.bin/console debug:event-dispatcher kernel.request shows listener priority. A security listener firing before tenant resolution is the usual culprit.Tenant::current() global. Inject the context service. Static tenant state is exactly what makes this class of package painful in tests.How can I help you explore Laravel packages today?