Installation
composer require cvele/multitenant-bundle
Enable the bundle in config/bundles.php:
return [
// ...
Cvele\MultiTenantBundle\CveleMultiTenantBundle::class => ['all' => true],
];
Configure Tenant Resolution
Define a tenant resolver service in config/services.yaml:
services:
App\Service\TenantResolver:
arguments:
$tenantParam: 'tenant' # URL parameter name
Register it as the default resolver in config/packages/cvele_multitenant.yaml:
cvele_multitenant:
resolver: App\Service\TenantResolver
First Use Case: Tenant-Aware Entity
Add the TenantAwareInterface and TenantAwareTrait to an entity:
use Cvele\MultiTenantBundle\Model\TenantAwareInterface;
use Cvele\MultiTenantBundle\Model\TenantAwareTrait;
class Product implements TenantAwareInterface
{
use TenantAwareTrait;
// ...
}
The bundle will now automatically filter queries and set the tenant ID.
Tenant Resolution
?tenant=acme to switch tenants dynamically.tenant() helper in controllers:
$tenant = $this->get('cvele_multitenant.tenant');
ON_KERNEL_REQUEST).Entity Integration
TenantAware entities auto-apply WHERE tenant_id = ?.$queryBuilder->andWhere('...')->disableTenantFiltering();
TenantAwareTrait::setTenant() for explicit tenant assignment:
$product->setTenant($tenant); // Overrides automatic resolution
Multi-Tenant Relationships
TenantAwareTrait for tenant-scoped data (e.g., Product).User if using FOSUserBundle).TenantAwareTrait for custom tenant resolution logic.API/CLI Context
X-Tenant-ID) or subdomains (acme.example.com).$this->tenantResolver->setTenant($tenantId);
Query Performance
$products = $repo->findBy([], ['tenant' => 'ASC']);
tenant_id is indexed in the database.Caching Quirks
$cache->get("tenant:{$tenantId}:products", ...);
Migration Risks
tenant_id as a nullable column initially to avoid downtime:
ALTER TABLE products ADD tenant_id INT NULL;
tenant_id for existing records.Symfony Version Mismatch
// Example: Override resolver for Symfony 5
$resolver = new TenantResolver($requestStack, 'tenant');
$this->tenantResolver = $resolver;
Tenant Resolution Logs Enable debug mode to trace tenant resolution:
cvele_multitenant:
debug: true
Check logs for Tenant resolved to: {id} entries.
Query Debugging Use Doctrine’s query logging to verify filtering:
doctrine:
dbal:
logging: true
profiling: true
Common Errors
TenantNotFoundException in controllers:
try {
$tenant = $this->tenantResolver->resolve();
} catch (TenantNotFoundException $e) {
return new JsonResponse(['error' => 'Invalid tenant'], 400);
}
TenantAware.Custom Resolvers
Implement Cvele\MultiTenantBundle\Resolver\TenantResolverInterface for:
acme.example.com).Dynamic Tenant Switching
Override the TenantAwareTrait to support runtime tenant changes:
class DynamicTenantAwareTrait {
public function setTenant(TenantInterface $tenant, bool $persist = true) {
$this->tenant = $tenant;
if ($persist) $this->markAsDirty('tenant_id');
}
}
Non-Database Tenants
Use the TenantInterface to support external tenant sources (e.g., Redis):
class RedisTenant implements TenantInterface {
public function getId() { /* ... */ }
public function getName() { /* ... */ }
}
Testing Mock the tenant resolver in PHPUnit:
$tenant = $this->createMock(TenantInterface::class);
$tenant->method('getId')->willReturn(1);
$this->tenantResolver->setTenant($tenant);
How can I help you explore Laravel packages today?