zhortein / multi-tenant-bundle
A fail-closed Symfony 7.4 LTS and Symfony 8.x multi-tenant bundle with PostgreSQL >= 16 defense-in-depth support.
Package info
github.com/Zhortein/multi-tenant-bundle
Type:symfony-bundle
pkg:composer/zhortein/multi-tenant-bundle
Requires
- php: >=8.3
- doctrine/dbal: ^3.8 || ^4.4
- doctrine/doctrine-bundle: ^2.19 || ^3.3
- doctrine/doctrine-migrations-bundle: ^3.4 || ^4.0
- doctrine/migrations: ^3.0
- doctrine/orm: ^3.5
- doctrine/persistence: ^3.3 || ^4.0
- psr/cache: ^3.0
- psr/event-dispatcher: ^1.0
- psr/log: ^3.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher: ^7.4 || ^8.0
- symfony/event-dispatcher-contracts: ^3.4
- symfony/filesystem: ^7.4 || ^8.0
- symfony/finder: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/messenger: ^7.4 || ^8.0
- symfony/service-contracts: ^3.4
Requires (Dev)
- aws/aws-sdk-php: ^3.371.5
- friendsofphp/php-cs-fixer: ^v3.75.0
- league/flysystem: ^3.30.2
- league/flysystem-aws-s3-v3: ^3.30.1
- monolog/monolog: ^3.0
- phpstan/phpstan: ^2.1
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/php-code-coverage: ^12.3.1
- phpunit/phpunit: ^12.2.5
- psr/simple-cache: ^3.0
- roave/security-advisories: dev-latest
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/dom-crawler: ^7.4 || ^8.0
- symfony/mailer: ^7.4 || ^8.0
- symfony/phpunit-bridge: ^7.4 || ^8.0
- symfony/scheduler: ^7.4 || ^8.0
- symfony/twig-bundle: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
Suggests
- aws/aws-sdk-php: Optional S3-compatible client and signing (^3.371.5); no Amazon hosting required
- league/flysystem: Optional object storage bridge (^3.30.2); not needed when disabled
- league/flysystem-aws-s3-v3: Optional S3-compatible protocol adapter (^3.30.1)
- monolog/monolog: Needed for the optional tenant-aware Monolog processor
- phpunit/phpunit: Needed by consumers extending the optional public test-kit base classes
- psr/simple-cache: Needed for PSR-16 simple cache tenant-aware decorators
- symfony/browser-kit: Needed by consumers extending the optional TenantWebTestCase
- symfony/dom-crawler: Needed by consumers making crawler-based requests with TenantWebTestCase
- symfony/mailer: Needed for dynamic tenant-aware mail sending
- symfony/scheduler: Needed to define recurring messages and redispatch them to persistent Messenger transports
- symfony/twig-bundle: Needed for templated tenant-aware emails
Provides
None
Conflicts
- psr/simple-cache: <3.0
Replaces
None
- dev-main
- dev-develop / 1.0.x-dev
- v1.0.0-rc.12
- v1.0.0-rc.11
- v1.0.0-rc.10
- v1.0.0-rc.9
- v1.0.0-rc.8
- v1.0.0-rc.7
- v1.0.0-rc.6
- v1.0.0-rc.5
- v1.0.0-rc.4
- v1.0.0-rc.3
- v1.0.0-rc.2
- v1.0.0-rc.1
- dev-release/1.0.0-rc.12
- dev-release/1.0.0-rc.12-wording
- dev-release/1.0.0-rc.12-preparation
- dev-feature/object-storage-audit-capabilities
This package is auto-updated.
Last update: 2026-09-13 09:05:23 UTC
README
A fail-closed Symfony 7.4 LTS and Symfony 8.x bundle for building multi-tenant applications, with PostgreSQL RLS as an optional defense in depth.
RC9 supports Symfony Scheduler's persistent RedispatchMessage path without
weakening tenant/global classification. Every main HTTP request, received
Messenger message, reused Console command, and
TenantExecutionBoundaryInterface callback starts from NONE.
TenantContext remains a shared mutable service; it is not recreated for every
request.
Features
- ๐ข Multiple Tenant Resolution Strategies: Subdomain, path-based, header-based, domain-based, DNS TXT, hybrid, or custom resolvers
- ๐๏ธ Database Strategies: Shared database with filtering or separate databases per tenant
- โก Performance Optimized: Built-in caching for tenant settings and configurations
- ๐ง Doctrine Integration: Automatic tenant filtering with Doctrine ORM
- ๐ง Tenant-Aware Services: Mailer with automatic tenant propagation, Messenger with context preservation, and file storage integration
- ๐ฏ Event-Driven: Database switching events and automatic tenant context resolution
- ๐ ๏ธ Advanced Commands: Schema management, migrations, and fixtures for tenants
- ๐งช Comprehensive Test Kit: First-class testing utilities to prove tenant isolation works
- ๐ RLS Integration: PostgreSQL Row-Level Security for defense-in-depth
- ๐ PHPStan Level Max: Static analysis at maximum level
Fail-closed security contract
Tenant-aware Doctrine reads and writes require a valid current tenant and reject invalid mappings, stale filter state, tenant changes, and cross-tenant mutations. Global ORM operations are explicit callbacks through GlobalDoctrineScopeInterface; direct filter disabling is outside the supported contract.
Messenger messages implement exactly one of TenantAwareMessageInterface or GlobalMessageInterface. Tenant-aware messages require consistent tenant metadata at send and receive time, while global messages must never carry a tenant stamp. See the RC1 to RC2 migration guide.
Messenger transport selection is explicit: tenant_transport preserves the historical per-tenant map/default behavior, while symfony_routing leaves transport stamps untouched so framework.messenger.routing and #[AsMessage] work natively. Native mode has no bundle fallback; an unrouted message with a handler may run synchronously. See Messenger and the RC7 to RC8 migration guide.
Persistent Symfony Scheduler work uses a classified application message inside
Symfony's RedispatchMessage, with an explicit persistent destination. Directly
scheduling the application message can execute its handler in the Scheduler
Worker because the Scheduler envelope is already marked as received. See the
Scheduler recipe and the RC8 to RC9 migration guide.
Installation
Install the bundle via Composer:
composer require "zhortein/multi-tenant-bundle:1.0.0-rc.9"
The core dependency set and optional Mailer, Twig, Monolog, and PSR-16 integrations are listed in the dependency classification.
Enable the bundle in your config/bundles.php:
<?php return [ // ... Zhortein\MultiTenantBundle\ZhorteinMultiTenantBundle::class => ['all' => true], ];
Quick Start
1. Create Your Tenant Entity
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Zhortein\MultiTenantBundle\Entity\TenantInterface; #[ORM\Entity] #[ORM\Table(name: 'tenants')] class Tenant implements TenantInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\Column(type: 'string', length: 255, unique: true)] private string $slug; #[ORM\Column(type: 'string', length: 255)] private string $name; // Implement TenantInterface methods... public function getId(): ?int { return $this->id; } public function getSlug(): string { return $this->slug; } public function setSlug(string $slug): void { $this->slug = $slug; } // ... other methods }
2. Configure the Bundle
Create config/packages/zhortein_multi_tenant.yaml:
zhortein_multi_tenant: tenant_entity: 'App\Entity\Tenant' database: strategy: 'shared_db' enable_filter: true rls: enabled: false fixtures: enabled: false mailer: enabled: false
3. Create Tenant-Aware Entities
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Zhortein\MultiTenantBundle\Attribute\AsTenantAware; use Zhortein\MultiTenantBundle\Entity\TenantAwareEntityTrait; #[ORM\Entity] #[AsTenantAware] class Product { use TenantAwareEntityTrait; #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\Column(type: 'string', length: 255)] private string $name; // ... other properties and methods }
4. Use in Controllers
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Zhortein\MultiTenantBundle\Context\TenantContextInterface; class DashboardController extends AbstractController { public function index(TenantContextInterface $tenantContext): Response { $tenant = $tenantContext->getTenant(); // All database queries are automatically filtered by tenant $products = $this->entityManager ->getRepository(Product::class) ->findAll(); // Only returns current tenant's products return $this->render('dashboard/index.html.twig', [ 'tenant' => $tenant, 'products' => $products, ]); } }
๐ Documentation
๐ Getting Started
- Installation & Setup - Complete installation guide
- Compatibility Policy - Tested PHP, Symfony, and Doctrine combinations
- Configuration Reference - All configuration options
- Database Strategies - Shared DB vs Multi-DB
๐๏ธ Core Concepts
- Tenant Context - Tenant resolution and access
- Tenant Resolution - Resolution strategies
- DNS TXT Resolver - DNS-based tenant resolution
- Domain Resolvers - Domain and hybrid resolvers
- Doctrine Tenant Filter - Automatic filtering
- Tenant Settings - Configuration system
๐ง Service Integration
- Mailer - Tenant-aware email with templated support
- Messenger - Tenant-aware queues with automatic context propagation
- Scheduler - Classified redispatch through a persistent transport
- Storage - Fail-closed file storage isolation
- Security Contract Migration - Required storage, cache, mailer, and observability migration
- RC4 to RC5 Migration - Persistent lifecycle reset and early/late HTTP resolution
- RC7 to RC8 Migration - Choose tenant-specific or native Symfony Messenger routing
- RC8 to RC9 Migration - Adopt fail-closed persistent Scheduler redispatch
- Persistent Process Lifecycle - Complete state inventory, reset order, and failure behavior
๐๏ธ Database Management
- Migrations - Database migrations
- Fixtures - Test data loading
๐ ๏ธ Development Tools
- CLI Commands - Console commands
- Testing - Testing strategies and Test Kit
- FAQ - Common questions
๐ Examples
- Basic Usage - Code examples
- Mailer Examples - Email templates and configuration
- Messenger Examples - Message routing and handling
- Service Integration - Practical implementations
Testing with the Bundle
The optional public Test Kit provides three intentionally small APIs:
TenantContextScopeexecutes a callback under a consumer-defined tenant and restores the previous context in all outcomes;TenantKernelTestCaseintegrates that scope with Symfony kernel tests;TenantWebTestCaseintegrates the same lifecycle with Symfony functional tests without selecting a resolver or database strategy.
Quick Example
<?php use App\Entity\Tenant; use Zhortein\MultiTenantBundle\Test\TenantKernelTestCase; final class ProductRepositoryTest extends TenantKernelTestCase { public function testTenantIsolation(): void { $tenantA = new Tenant("tenant-a"); $tenantB = new Tenant("tenant-b"); self::assertSame( ["A product"], $this->withTenant($tenantA, fn (): array => $this->repository->findForTenant($tenantA)), ); self::assertSame( ["B product"], $this->withTenant($tenantB, fn (): array => $this->repository->findForTenant($tenantB)), ); } }
Running Tests
# Run the complete bundle suite make test # Run unit and integration subsets make test-unit make test-integration # Run effective PostgreSQL RLS isolation tests make test-with-postgres
See the Testing Documentation for installation, lifecycle, and consumer examples.
Code Quality
# PHPStan at maximum level make phpstan # PHP-CS-Fixer code style check make csfixer-check # Fix code style make csfixer # Run all quality checks make dev-check
Contributing
- Fork the repository
- Create a feature branch
- Write tests for your changes
- Ensure all tests pass and code meets quality standards
- Submit a pull request
See CONTRIBUTING.md for detailed guidelines.
License
This bundle is released under the MIT License. See the LICENSE file for details.
Support
- Documentation: Complete documentation
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Object storage
The optional object storage core provides durable tenant-aware references and a backend-independent contract, disabled by default. The optional Flysystem/S3-compatible bridge adds real MinIO proofs and leaves the historical file API unchanged. RC12 remains a prerelease. Its optional audit API adds lazy location inventory, explicit scopes and logical identity observations. Audit is disabled by default; historical objects without identity remain readable. Upgrading enables no integration and runs no SQL, object or metadata migration. See the RC11 to RC12 migration guide, including the trust boundary, progressive adoption and application rollback.
Changelog
See CHANGELOG.md for version history and upgrade instructions.