phil / tenancy-bundle
Active tenant for multi-tenant Symfony applications: resolved from the tenants an account can reach or from the host, switched without a round trip to the identity provider, a fail-closed Doctrine filter, the tenant carried across Messenger, sessions, mail and cache pools, shops by domain, and Postg
Package info
github.com/parler-haut-interagir-librement/tenancy-bundle
Type:symfony-bundle
pkg:composer/phil/tenancy-bundle
Requires
- php: >=8.5
- doctrine/dbal: ^4.2
- doctrine/orm: ^3.5
- doctrine/persistence: ^4.1
- phil/ddd-bundle: ^0.1@dev
- phil/url: ^0.1@dev
- psr/event-dispatcher: ^1.0
- psr/log: ^3.0
- symfony/config: ^8.2
- symfony/dependency-injection: ^8.2
- symfony/http-foundation: ^8.2
- symfony/http-kernel: ^8.2
- symfony/routing: ^8.2
Requires (Dev)
- doctrine/doctrine-bundle: ^3.3
- phpunit/phpunit: ^13.4
- psr/cache: ^3.0
- symfony/browser-kit: ^8.2
- symfony/cache: ^8.2
- symfony/cache-contracts: ^3.6
- symfony/console: ^8.2
- symfony/event-dispatcher: ^8.2
- symfony/framework-bundle: ^8.2
- symfony/mailer: ^8.2
- symfony/messenger: ^8.2
- symfony/mime: ^8.2
- symfony/security-bundle: ^8.2
- symfony/security-core: ^8.2
- symfony/security-http: ^8.2
- twig/twig: ^3.21
Suggests
- symfony/cache: To scope cache.app, and the pools built on it, to the active tenant
- symfony/console: For the --tenant option of every command, and the commands of row-level security
- symfony/mailer: For the transport, the sender and the reply address of each tenant
- symfony/messenger: To carry the active tenant across transports
- symfony/security-http: For the sessions scoped to the tenant their accounts signed in on
- twig/twig: For the active_tenant() and reachable_tenants() functions
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-15 03:40:34 UTC
README
Which tenant an account acts in right now, switched instantly and enforced in every query.
phil/tenancy-bundle manages the active tenant of a multi-tenant Symfony application, whether the account chooses it among the tenants an external identity provider lets it reach or the request's host decides it. It validates opaque tenant references and resolves the tenant the current account acts in, from its remembered choice and the tenants it can reach. A tenant switch is checked against that set with no round trip to the identity provider. A Doctrine filter keeps queries inside the active tenant and fails closed. The bundle does not keep the active tenant in a token or in the identity provider's session, decide where an account's entitlements come from, or render the selector page.
It is for applications where one account belongs to several tenants, such as two organisations or two shops. The identity provider knows which tenants an account can reach, and its session spans every application that shares it. The active tenant belongs to one application, so it stays there. Switching keeps working while the provider is down, and resolution never guesses: with several tenants and no usable choice, the account selects.
It is a Symfony bundle on Doctrine ORM and phil/ddd-bundle. It wires the choice remembered in the application's
session, a request listener that sends an account that must choose to the selector, a same-origin switch, a
#[CurrentTenant] controller argument, the tenant carried across Messenger transports, a --tenant option on every
command, and two Twig functions. Several sites can live in one application: each host, and each alias of it, serves
its tenant, shops by domain share one checkout and one customer account, a session signed in on one tenant is not
signed in on another, a tenant can sign its members in with its own firewall and load them from its own user provider,
and each tenant's mail leaves through its own transport and sender. Each tenant reads and writes its own part of
cache.app and of the pools built on it, tags included. Messenger, the Console component, Twig, the Security
component, the Mailer component and the Cache component are optional. The application supplies the
reachable tenants through the ReachableTenantsProvider port, and the store of the choice is a port as well.
Status
| Part | State |
|---|---|
TenantId, ReachableTenants |
Implemented and tested |
ActiveTenantRule, Resolution, Outcome, TenantNotReachable, NoActiveTenant |
Implemented and tested |
TenantScoped, ActiveTenantFilter, MisconfiguredTenantScope |
Implemented and tested; the filter is registered enabled |
ActiveTenant, TenantContext, TenantActivation, DoctrineTenantActivation |
Implemented and tested |
SessionActiveTenantStore |
Implemented and tested; the default store |
ResolveActiveTenant, SwitchActiveTenant, #[CurrentTenant] |
Implemented and tested |
TenantMiddleware, TenantStamp, TenantOption, TenancyExtension |
Implemented and tested; registered when their component is installed |
HostTenants, ConfiguredHostTenants, ResolveHostTenant, HostTenantResolved |
Implemented and tested; registered with hosts |
HostAliases, RedirectHostAlias |
Implemented and tested; registered with host_aliases |
ShopNetwork, OrderOrigin, ShopContext |
Implemented and tested |
AccountSharing, ConfiguredAccountSharing, TenantScopedUserProvider, RememberSignInTenant |
Implemented and tested; off until authentication is enabled |
TenantRequestMatcher, TenantUserProviders |
Implemented and tested; registered with authentication.tenant_firewalls and tenant_user_providers |
TenantMailSettings, ConfiguredTenantMailSettings, TenantMail, TenantTransports |
Implemented and tested; registered with mailer settings |
TenantScopedPool, TenantScopedTagAwarePool, TenantCacheNamespace, TenantCachePass |
Implemented and tested; cache.app and the pools built on it scoped when symfony/cache is installed |
ReachableTenantsProvider |
Port; the application implements it |
TenantPlacement, Placement |
Port and value object; a database bundle or the application implements the port |
PhilTenancyBundle |
Configuration phil_tenancy |
Documentation
- Installation: requirements, installing, registering and configuring the bundle, the switch route, and several entity managers.
- Configuration: every key, what each registers, what the application still writes, and the bundle's constants.
- Integrations: the tenant context and its activations, the request listener, the switch,
#[CurrentTenant], Messenger, the console and Twig. - Hosts and shops: the tenant of each host, the aliases of a host and their redirect, the shop network, the shop of a request and the origin of an order.
- Authentication per tenant: sessions scoped to the tenant their accounts signed in on, shared accounts, a firewall for each tenant and a user provider for each tenant.
- Mail per tenant: each tenant's transport, sender and reply address.
- Cache per tenant:
cache.appand the pools built on it scoped to the active tenant, a sub-namespace and prefixed tags for each tenant, the pools every tenant shares, and the behaviour in a worker. - The active tenant: why the active tenant belongs to the application,
TenantIdand its refusals,ReachableTenants, theActiveTenantservice and a request from end to end. - Resolution and switching: the resolution table, the switch rule,
Resolution,Outcomeand the selector's rules. - The Doctrine filter:
#[TenantScoped], the SQL the filter adds, failing closed, cross-tenant work, mapping errors and the filter's limits. - Row-level security: the row-level security of PostgreSQL behind the filter, the session variable of each connection, the generated policies, the role and the check command.
- Extension points: the ports method by method, the placement of a tenant's data among them, their invariants and example adapters.
- Testing: how the bundle is tested, how to run its tests, and how an application tests its adapters and its isolation.
- Reference: the public API by layer, one line per type, with its state.
Quick start
composer require phil/tenancy-bundle
# config/packages/phil_tenancy.yaml phil_tenancy: reachable_tenants: App\Tenancy\EntitlementsReachableTenants # your adapter request: selector_route: tenant_select
#[ORM\Entity] #[TenantScoped] // the field `tenant` holds the tenant reference class Order { /* … */ } // Every query of the request now sees the active tenant's orders only. public function orders(#[CurrentTenant] TenantId $tenant, OrderRepository $orders): Response { /* … */ }
Requirements
PHP 8.5 or later; doctrine/orm 3.5 or later; phil/ddd-bundle 0.1; symfony/config,
symfony/dependency-injection, symfony/http-foundation, symfony/http-kernel and symfony/routing 8.2 or
later; phil/url 0.1; psr/event-dispatcher 1.0. symfony/console, symfony/messenger, symfony/security-http,
symfony/mailer and twig/twig are suggested.
Contributing
Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.
License
MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.