🎯 Tenant Lifecycle Events
The Multi-Tenancy Bundle provides a comprehensive event system that fires during key tenant lifecycle operations. This enables extensibility and integration with external services without modifying the bundle's core code.
📋 Overview​
All tenant events extend the AbstractTenantEvent base class and provide:
- Tenant Identifier: The unique identifier of the affected tenant
- Tenant Configuration: The
TenantConnectionConfigDTOwhen available - Timestamp: When the event occurred (
DateTimeImmutable)
🚀 Available Events​
TenantCreatedEvent​
Dispatched when a new tenant database is created.
Fired from: CreateDatabaseCommand after successful database creation
Properties:
getTenantIdentifier(): The tenant's unique identifiergetTenantConfig(): The tenant connection configuration DTOgetDatabaseName(): Name of the created databasegetOccurredAt(): Timestamp of the event
Use Cases:
- Trigger billing/subscription setup
- Send welcome notifications
- Initialize external services for the tenant
- Log tenant creation for auditing
use Hakam\MultiTenancyBundle\Event\TenantCreatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TenantCreatedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [TenantCreatedEvent::class => 'onTenantCreated'];
}
public function onTenantCreated(TenantCreatedEvent $event): void
{
$tenantId = $event->getTenantIdentifier();
$dbName = $event->getDatabaseName();
// Initialize billing for the new tenant
$this->billingService->createSubscription($tenantId);
// Send welcome email
$this->mailer->sendWelcomeEmail($tenantId);
}
}
TenantDeletedEvent​
Dispatched when a tenant database is dropped/deleted.
Fired from: DbService::dropDatabase() after successful database deletion
Properties:
getTenantIdentifier(): The tenant's unique identifiergetDatabaseName(): Name of the deleted databasegetOccurredAt(): Timestamp of the event
Use Cases:
- Clean up external resources
- Cancel billing/subscriptions
- Archive tenant data
- Send deletion notifications
- Update monitoring/logging systems
use Hakam\MultiTenancyBundle\Event\TenantDeletedEvent;
class TenantDeletedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [TenantDeletedEvent::class => 'onTenantDeleted'];
}
public function onTenantDeleted(TenantDeletedEvent $event): void
{
$tenantId = $event->getTenantIdentifier();
// Cancel subscription
$this->billingService->cancelSubscription($tenantId);
// Clean up external storage
$this->storageService->deleteTenantFiles($tenantId);
}
}
TenantMigratedEvent​
Dispatched when migrations have been executed on a tenant database.
Fired from: MigrateCommand after successful migration execution
Properties:
getTenantIdentifier(): The tenant's unique identifiergetTenantConfig(): The tenant connection configuration DTOgetMigrationType(): Either'init'or'update'isInitialMigration(): Returnstrueif this is the first migrationisUpdateMigration(): Returnstrueif this is an update migrationgetToVersion(): Target migration version (if specified)getOccurredAt(): Timestamp of the event
Constants:
TenantMigratedEvent::TYPE_INIT='init'TenantMigratedEvent::TYPE_UPDATE='update'
Use Cases:
- Log migration history
- Trigger post-migration tasks (cache warming, etc.)
- Notify administrators of schema changes
- Update tenant status in external systems
use Hakam\MultiTenancyBundle\Event\TenantMigratedEvent;
class TenantMigratedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [TenantMigratedEvent::class => 'onTenantMigrated'];
}
public function onTenantMigrated(TenantMigratedEvent $event): void
{
if ($event->isInitialMigration()) {
$this->logger->info('Tenant database initialized', [
'tenant' => $event->getTenantIdentifier(),
'version' => $event->getToVersion(),
]);
}
// Clear cached schema information
$this->cacheService->clearTenantSchemaCache($event->getTenantIdentifier());
}
}
TenantBootstrappedEvent​
Dispatched when a tenant database has been bootstrapped with initial fixture data.
Fired from: LoadTenantFixtureCommand after successful fixture loading
Properties:
getTenantIdentifier(): The tenant's unique identifiergetTenantConfig(): The tenant connection configuration DTO (may be null)getLoadedFixtures(): Array of loaded fixture class namesgetOccurredAt(): Timestamp of the event
Use Cases:
- Notify tenant that environment is ready
- Trigger post-setup tasks
- Initialize default configurations
- Send onboarding emails
use Hakam\MultiTenancyBundle\Event\TenantBootstrappedEvent;
class TenantBootstrappedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [TenantBootstrappedEvent::class => 'onTenantBootstrapped'];
}
public function onTenantBootstrapped(TenantBootstrappedEvent $event): void
{
$tenantId = $event->getTenantIdentifier();
$fixtures = $event->getLoadedFixtures();
$this->logger->info('Tenant environment ready', [
'tenant' => $tenantId,
'fixtures_loaded' => count($fixtures),
]);
// Send "Your account is ready" notification
$this->notificationService->sendTenantReadyNotification($tenantId);
}
}
TenantSwitchedEvent​
Dispatched when the active tenant database connection is switched.
Fired from: DbSwitchEventListener after successful connection switch
Properties:
getTenantIdentifier(): The new tenant's identifiergetTenantConfig(): The new tenant's connection configuration DTOgetPreviousTenantIdentifier(): Previous tenant identifier (or null)getPreviousDatabaseName(): Previous database name (or null)hadPreviousTenant(): Returnstrueif there was a previous tenantgetOccurredAt(): Timestamp of the event
Use Cases:
- Track tenant access for analytics
- Update request context
- Clear tenant-specific caches
- Log tenant access for auditing/security
The bundle's TenantContext service already listens to this event and tracks the current tenant identity. When Cache Isolation is enabled, cache keys are automatically scoped based on this context.
use Hakam\MultiTenancyBundle\Event\TenantSwitchedEvent;
class TenantSwitchedSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [TenantSwitchedEvent::class => 'onTenantSwitched'];
}
public function onTenantSwitched(TenantSwitchedEvent $event): void
{
// Log tenant access
$this->analyticsService->trackTenantAccess($event->getTenantIdentifier());
// Clear previous tenant's cache if switching
if ($event->hadPreviousTenant()) {
$this->cacheService->clearTenantCache($event->getPreviousTenantIdentifier());
}
}
}
💡 Complete Example: Multi-Purpose Subscriber​
Here's a complete example of a subscriber that handles all tenant lifecycle events:
<?php
namespace App\EventSubscriber;
use Hakam\MultiTenancyBundle\Event\AbstractTenantEvent;
use Hakam\MultiTenancyBundle\Event\TenantBootstrappedEvent;
use Hakam\MultiTenancyBundle\Event\TenantCreatedEvent;
use Hakam\MultiTenancyBundle\Event\TenantDeletedEvent;
use Hakam\MultiTenancyBundle\Event\TenantMigratedEvent;
use Hakam\MultiTenancyBundle\Event\TenantSwitchedEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TenantLifecycleSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly BillingServiceInterface $billingService,
private readonly NotificationServiceInterface $notificationService,
) {
}
public static function getSubscribedEvents(): array
{
return [
TenantCreatedEvent::class => ['onTenantCreated', 0],
TenantDeletedEvent::class => ['onTenantDeleted', 0],
TenantMigratedEvent::class => ['onTenantMigrated', 0],
TenantBootstrappedEvent::class => ['onTenantBootstrapped', 0],
TenantSwitchedEvent::class => ['onTenantSwitched', -10],
];
}
public function onTenantCreated(TenantCreatedEvent $event): void
{
$this->log('tenant.created', $event, [
'database_name' => $event->getDatabaseName(),
]);
$this->billingService->createSubscription(
$event->getTenantIdentifier(),
$event->getTenantConfig()
);
}
public function onTenantDeleted(TenantDeletedEvent $event): void
{
$this->log('tenant.deleted', $event, [
'database_name' => $event->getDatabaseName(),
]);
$this->billingService->cancelSubscription($event->getTenantIdentifier());
}
public function onTenantMigrated(TenantMigratedEvent $event): void
{
$this->log('tenant.migrated', $event, [
'migration_type' => $event->getMigrationType(),
'version' => $event->getToVersion(),
]);
if ($event->isInitialMigration()) {
$this->notificationService->notifyTenantReady($event->getTenantIdentifier());
}
}
public function onTenantBootstrapped(TenantBootstrappedEvent $event): void
{
$this->log('tenant.bootstrapped', $event, [
'fixtures_count' => count($event->getLoadedFixtures()),
]);
}
public function onTenantSwitched(TenantSwitchedEvent $event): void
{
if ($event->hadPreviousTenant()) {
$this->log('tenant.switched', $event, [
'from_tenant' => $event->getPreviousTenantIdentifier(),
]);
}
}
private function log(string $eventName, AbstractTenantEvent $event, array $extra = []): void
{
$this->logger->info("Tenant lifecycle: {$eventName}", array_merge([
'tenant_identifier' => $event->getTenantIdentifier(),
'occurred_at' => $event->getOccurredAt()->format('c'),
], $extra));
}
}
📊 Event Execution Order​
The events fire in the following logical order during tenant setup:
- TenantCreatedEvent — Database created
- TenantMigratedEvent (type: init) — Initial schema applied
- TenantBootstrappedEvent — Fixtures loaded
- TenantSwitchedEvent — Every time the connection switches
For updates:
- TenantMigratedEvent (type: update) — Schema updated
For teardown:
- TenantDeletedEvent — Database dropped
⚡ Performance Considerations​
- Events are dispatched synchronously by default
- Keep event listeners lightweight to avoid impacting command execution
- For heavy processing, consider dispatching async messages from your listeners
- The
TenantSwitchedEventfires on every context switch — use it judiciously
✅ Registering Subscribers​
Subscribers are automatically registered when using Symfony's autoconfigure:
# config/services.yaml
services:
_defaults:
autoconfigure: true
App\EventSubscriber\:
resource: '../src/EventSubscriber/'
Or explicitly:
services:
App\EventSubscriber\TenantLifecycleSubscriber:
tags: ['kernel.event_subscriber']
🔄 Backward Compatibility​
This event system is fully backward compatible. Existing applications that don't subscribe to these events will continue to work without any changes.