swp/multi-tenancy-bundle
SWP MultiTenancyBundle provides flexible multi-tenant support for PHP applications, with tenant-aware services, configuration, and integration helpers for isolating data and switching tenants. Ideal for SaaS projects needing clean tenant separation.
First, a reality check: despite the "bundle" suffix, this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/multi-tenancy.php. If you are a Laravel developer, you will not composer require this into a Laravel app and get working tenancy — you will be reading it as a reference implementation of host-based tenant resolution, or working inside a Symfony/Superdesk Web Publisher codebase. Treat the "day-to-day leverage" as architectural first, code-level second.
Minimal steps if you are in a Symfony host app:
composer require swp/multi-tenancy-bundle — note the last tagged release is June 2018, so expect Symfony 3.x/4.x-era constraints. Check composer.json require before anything else; a modern Symfony 6/7 app will need --ignore-platform-reqs gymnastics or a fork.AppKernel::registerBundles() or config/bundles.php for Symfony 4 Flex).app/config/config.yml — look at DependencyInjection/Configuration.php in the repo for the exact tree (tenant entity class, resolution strategy, theme paths).SuperdeskWebPublisher/SWP, not here. Read the parent repo's docs — the split repo has almost no README of its own.First use case to validate end-to-end: resolve a tenant from the request host, inject that tenant into a controller/service, and render a tenant-specific template. In Symfony terms that means hitting a route with Host: acme.example.com and confirming the tenant context service returns the acme tenant and the theme/template lookup switched. If you cannot get that loop closed in an hour, stop — the bundle's assumptions (Doctrine entities, organization model, theme layout) are probably too opinionated for your app.
Host/subdomain resolution. The core pattern is a request listener that inspects $request->getHost() (and sometimes a path prefix or header) and sets the current tenant on a context service. The Laravel analogue is a TenantMiddleware that runs before SubstituteBindings. If you are porting the idea, replicate the ordering: resolution must happen before routing decisions that depend on tenant, and before any DB query that should be tenant-scoped.
Tenant context as a scoped service. The bundle exposes a "current tenant" object that other services consume. In Laravel you'd bind this as a singleton reset per request (or use scoped() in the container). The important discipline is: never pass tenant IDs around manually — read from context. That keeps controllers, templates, and repositories uniform.
Doctrine filtering for row-level isolation. The typical mechanism is a Doctrine SQL filter or a custom repository that appends tenant_id = :current. This is the piece most worth studying: the filter must be enabled on every connection (including the one used by console commands and by Messenger consumers), and must be disabled explicitly for cross-tenant admin queries. In Laravel the equivalent is a global scope on Eloquent models, with withoutGlobalScope() escape hatches — same failure mode: the scope is silently absent in queued jobs.
Theme/template switching per tenant. The bundle's Superdesk lineage means it carries a theme-resolution concern: a tenant maps to a template directory or theme package. Pattern: resolve tenant → resolve theme name → point the Twig loader / view finder at a tenant-specific path with a shared fallback. In Laravel: view()->addNamespace() or a custom ViewFinder that prepends resources/views/tenants/{slug}.
Integration tips for a Laravel port:
tenants table with domain, subdomain, slug, settings (JSON), and a theme column.Cache::rememberForever("tenant:host:$host")) and bust it on tenant save.tenant_id column if you are porting this bundle's approach (it assumes shared schema + filter); use schema-per-tenant only if you are deliberately diverging.TenantManager that supports run($tenant, fn () => ...) for jobs, commands, and tests — this is the single most valuable abstraction and it is what makes the rest testable.*.test) with valet/herd so acme.test and beta.test both resolve during development.Testing pattern. Every feature test should be able to say $this->actingAsTenant($tenant) or wrap the assertion in Tenant::run($tenant, ...). If the bundle's context is a mutable singleton, forgetting to reset it between tests produces cross-test bleed — write a base TestCase::tearDown() that resets the context unconditionally.
git clone of this repo gives you a slice with no history of the surrounding bundles. Bugs must be filed against SuperdeskWebPublisher/SWP; do not expect merges here.doctrine:fixtures:load, and off inside Messenger/queue workers unless you explicitly enable it. Always assert isolation with a test that queries without setting a tenant and expects zero rows.$request->getHost() is proxy- and port-sensitive. Behind a load balancer, configure trusted proxies (Request::setTrustedProxies) or your tenant resolution will silently fall back to a default tenant. Strip ports: acme.test:8000 vs acme.test will not match naive equality checks.RequestTerminated event, not in the controller.cache:clear and tag collisions become cross-tenant data leaks. Prefix tenant:{id}: on every key, including the view cache.warning when resolution falls back.CompilerPass in DependencyInjection/ before assuming a service is overridable via config.stancl/tenancy or spatie/laravel-multitenancy cover the same ground with active maintenance; use this bundle only to sanity-check your resolution and theme-switching design, and port the ideas, not the code.How can I help you explore Laravel packages today?