syndicatetheory/multi-tenant-bundle
A Symfony bundle for multi-tenant PHP applications, providing tenant-aware configuration and infrastructure to isolate data and serve multiple customers from one codebase, simplifying SaaS architecture.
First, the orientation check. Despite the framing, this is not a Laravel package. It is a Symfony2 bundle (PHP 5.x era, Symfony 2.x), marked [IN DEVELOPMENT], with 13 stars, zero dependents, and a score of 0.225. You cannot composer require it into a Laravel app and expect anything to work — the service container, event dispatcher, Doctrine integration, and Resources/config/*.yml conventions are all Symfony. Treat this repository as a design reference for multi-tenancy, not a dependency.
Where to look first, in this order:
composer.json — confirm the Symfony/PHP constraints. If it pins symfony/symfony: ~2.x, that branch is EOL and unpatched; nothing here belongs in production.README.md — expect thin or absent docs given the star count and in-development banner. Do not budget time for it to answer your questions.DependencyInjection/*Extension.php and Resources/config/services.yml — this is where the bundle's real configuration surface lives (tenant resolution strategy, connection parameters, storage paths). This tells you what knobs the author actually thought about.EventListener/ or EventSubscriber/ — the request-time hook where a tenant is resolved and applied. This is the single most portable idea in the repo.Connection wrapper, a wrapper_class config, or an event listener on kernel.request that swaps connection parameters. That is the mechanism you are really evaluating.The first use case to test in Laravel: database-per-tenant with hostname resolution. Wire a middleware that reads $request->getHost(), looks up the tenant, and rewrites config(['database.connections.tenant' => [...]]) before calling DB::purge('tenant') and DB::reconnect('tenant'). If you can get that working with two local hosts, you have reproduced ~80% of what this bundle is trying to do.
Resolution at the edge, application below. The bundle's model is a request listener that identifies a tenant before anything else runs. In Laravel that maps to middleware registered in the web group before StartSession, Authenticate, and anything touching the database. If tenancy resolution runs after auth, your user lookup hits the wrong database.
Connection switching, not connection duplication. The Symfony pattern is to mutate connection parameters and let Doctrine rebuild. The Laravel equivalent is configuration mutation plus purge/reconnect — never DB::setDefaultConnection() alone, because pooled connections already open will keep pointing at the old tenant. Order matters: DB::purge($name) → reconfigure → DB::reconnect($name) → DB::setDefaultConnection($name).
Per-tenant namespacing for everything stateful. Beyond the database, the bundle concerns itself with cache and filesystem paths. Mirror that deliberately: Storage::disk('tenant') with a root of storage/tenants/{id}, cache prefixes per tenant, and a session cookie scoped to the tenant's domain. Any subsystem that holds global state and isn't namespaced is where your first cross-tenant leak will appear.
Console and queue symmetry. Anything the web request does, the CLI must do too. Port the pattern to an artisan command that iterates tenants, sets context, runs a callback, and tears down — php artisan tenants:run migrate. Queue workers need a job middleware that re-establishes tenant context from a tenant ID carried in the job payload, not from a global, because workers are long-lived.
Shared-schema vs. database-per-tenant is a fork in the road, not a detail. If the bundle only supports separate databases, know that up front. Shared-schema with a tenant_id column and a global Eloquent scope is a fundamentally different implementation with different failure modes (a forgotten scope fails open; a wrong connection fails closed). Don't start down one path assuming you can convert later cheaply.
Integration tip: vendor the ideas, not the code. Extract the resolution and connection-swap logic into your own app/Tenancy/ namespace and own it. Given zero dependents and an in-development banner, you will be maintaining every line you pull in regardless.
[IN DEVELOPMENT] label plus 0 dependents is the headline. No downstream users means no one has hit the edge cases for you, and no issue reports to learn from. There is no realistic path to upstream support. This is a read-only artifact.services.yml parameters) will look foreign to anyone on Laravel or Symfony 6+. Read for architecture, not for style.config() shows the right database name, but queries still hit the wrong place. Cause: an already-resolved connection object. Always purge before reconfiguring, and log DB::getDefaultConnection() and DB::connection()->getDatabaseName() at the top of every tenant-scoped request while you're building.finally block, not an afterthought.Configuration.php (if present) and the DI extension to see what is actually configurable versus hardcoded, before designing anything around it.config/tenancy.php and registers singletons; that is the seam you want in your port, because it's where you'll later swap resolution strategies.composer.json under any circumstances.How can I help you explore Laravel packages today?