customable/tenancy-bundle (3.0.10)

Published 2026-03-17 12:13:05 +00:00 by jack

Installation

{
	"repositories": [{
			"type": "composer",
			"url": ""
		}
	]
}
composer require customable/tenancy-bundle:3.0.10

About this package

Multi-tenant Symfony bundle with schema-per-tenant support for PostgreSQL

Customable Tenancy Bundle

A production-ready multi-tenant bundle for Symfony 7.3+ / 8.0+ applications.

Inspired by Tenancy for Laravel and Sprout Laravel.

Requirements

  • PHP 8.4+
  • Symfony 7.3+ or 8.0+
  • Doctrine ORM 3.0+
  • PostgreSQL 12+, MySQL 8.0+, or MariaDB 10.5+

Installation

composer require customable/tenancy-bundle

Quick Start

1. Create your Tenant entity:

use Customable\TenancyBundle\Contract\TenantInterface;
use Customable\TenancyBundle\Trait\HasTenantData;

#[ORM\Entity]
class Tenant implements TenantInterface
{
    use HasTenantData;

    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    public readonly ?int $id;

    #[ORM\Column(unique: true)]
    public string $identifier = '';

    public string|int $key {
        get => $this->id ?? 0;
    }

    public string $schema {
        get => 'tenant_' . $this->identifier;
    }
}

2. Create your repository:

use Customable\TenancyBundle\Repository\AbstractTenantRepository;

class TenantRepository extends AbstractTenantRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Tenant::class);
    }
}

3. Configure:

# config/packages/tenancy.yaml
tenancy:
    tenant_class: App\Entity\Tenant
    strategy:
        type: schema          # schema, database, or column
    identification:
        central_domains: [example.com]
    routing:
        domain_pattern: "{tenant}.example.com"

4. Use in controllers:

use Customable\TenancyBundle\Attribute\CurrentTenant;

#[Route('/dashboard')]
public function dashboard(#[CurrentTenant] TenantInterface $tenant): Response
{
    return new Response("Welcome to {$tenant->identifier}!");
}

Features

Isolation Strategies

Strategy How it works Best for
Schema PostgreSQL schemas / MySQL databases per tenant Strong isolation, recommended
Database Separate database connections per tenant Maximum isolation
Column Shared tables with automatic tenant_id filtering Simple setups, fewer tenants

Tenant Identification

8 built-in resolvers, configurable priority chain:

  • Subdomainacme.example.com
  • Domainacme-corp.de
  • HeaderX-Tenant: acme
  • Path/tenant/acme/...
  • Query?tenant=acme
  • Cookie / Session / Request body

Bootstrappers

12 bootstrappers for automatic resource isolation:

Bootstrapper Isolates
Database Schema/database switching
Cache Key prefixing
Filesystem Path isolation
Session Session isolation
Cookie Cookie domain/path
Mail Sender address
Broadcasting Channel prefix
RootUrl Application URL
TenantConfig Runtime parameters
DatabaseCache DB-backed cache
PersistentQueue Messenger workers
PostgresRLS Row-Level Security

Console Commands

# Tenant management
tenancy:tenant:create acme "Acme Corp"
tenancy:tenant:list
tenancy:tenant:delete acme
tenancy:tenant:clone acme acme-staging          # clone with data + files
tenancy:tenant:clone acme demo --no-data        # schema only
tenancy:tenant:clone acme test --no-files       # skip filesystem

# Run any command in tenant context
tenancy:run acme cache:clear
tenancy:run all doctrine:schema:validate

# Migrations (entity-aware: #[AsTenantEntity] / #[AsCentralEntity] / #[AsSharedEntity])
tenancy:make:migration              # auto-detect diffs from entity attributes
tenancy:make:migration --central    # central migration only
tenancy:make:migration --tenant     # tenant migration only
tenancy:make:migration --empty      # empty migration template
tenancy:migrate                     # full deployment: central first, then all tenants
tenancy:migrate acme                # migrate specific tenant only
tenancy:migrate --central-only      # central migrations only
tenancy:migrate --tenant-only       # tenant migrations only
tenancy:migration:status
tenancy:migration:rollback acme --version=prev

# Maintenance mode
tenancy:maintenance enable acme --message="Upgrading" --until="+2 hours"
tenancy:maintenance disable acme
tenancy:maintenance status acme

# Tenant scheduling
tenancy:schedule:run                              # run all due tasks
tenancy:schedule:list acme                        # list tasks for tenant

# GDPR data export
tenancy:data:export acme                          # JSON export
tenancy:data:export acme --format=csv             # CSV export
tenancy:data:export acme --format=sql             # SQL export
tenancy:data:export acme --include-files -o /tmp/export.zip  # with files

# Domain DNS verification
tenancy:domain:verify acme acme-corp.de           # verify domain ownership

# Soft delete & recovery
tenancy:tenant:recover acme            # recover soft-deleted tenant
tenancy:tenant:purge --expired         # permanently delete expired tenants

# Performance
tenancy:performance:analyze acme --show-indexes --show-pool

# Backup
tenancy:backup acme
tenancy:backup          # all tenants

# Cache
tenancy:cache:warmup acme
tenancy:cache:warmup    # all tenants

# Health check
tenancy:health:check

Tenant Lifecycle

// Suspend a tenant (blocks access with 503)
$lifecycleManager->suspend($tenant, 'Payment overdue');

// Check state
$lifecycleManager->isSuspended($tenant);  // true
$lifecycleManager->getState($tenant);     // TenantState::Suspended

// Reactivate
$lifecycleManager->unsuspend($tenant);

// Soft-delete with retention period (schema preserved)
$softDeleteManager->softDelete($tenant, retentionDays: 30);
$softDeleteManager->isSoftDeleted($tenant);  // true
$softDeleteManager->getPurgeDate($tenant);   // DateTimeImmutable

// Recover before retention expires
$softDeleteManager->recover($tenant);

// Purge all expired (hard-delete after retention)
$softDeleteManager->purgeExpired();

Plan/Tier Management

// Define plans via PlanRegistryInterface
$planManager->assignPlan($tenant, 'pro');
$planManager->getCurrentPlan($tenant);     // TenantPlan
$planManager->upgradePlan($tenant, 'enterprise');

// Plans auto-update tenant features (maxUsers, storageQuotaMb, etc.)
// Works with Feature Gates (#[RequiresFeature])

Maintenance Mode

// Enable with optional expiry
$maintenanceManager->enable($tenant, 'Upgrading database', new DateTimeImmutable('+2 hours'));

// Auto-disables when expiry passes
$maintenanceManager->isInMaintenance($tenant);

Rate Limiting & Quotas

// Rate limiting
$result = $rateLimiter->consume('api');
if (!$result->allowed) {
    // $result->retryAfterSeconds
}

// Quota management
$quotaManager->recordUsage($tenant, 'api_calls', 1);
$usage = $quotaManager->getUsage($tenant, 'api_calls');
// $usage->currentUsage, $usage->limit, $usage->percentage

API Key Management

// Create key (default: cache-backed, override with DB implementation)
['key' => $rawKey, 'info' => $info] = $apiKeyManager->createKey($tenant, 'My API Key', ['read', 'write']);

// Validate
$info = $apiKeyManager->validateKey($rawKey);

Tenant Notifications

Automatic notifications to tenant admins on important lifecycle events:

// Implement your notification channel
class EmailTenantNotifier implements TenantNotifierInterface
{
    public function notify(TenantInterface $tenant, TenantNotification $notification): void
    {
        // Send email, Slack message, webhook, etc.
    }
}

// Manual notification
$dispatcher->dispatch($tenant, TenantNotification::warning('Storage Warning', 'Usage at 80%'));

Auto-notifications fire on: storage limit warnings, tenant suspension/reactivation, and storage limit exceeded events.

Tenant Health Score

Aggregated health score (0-100) from pluggable metric providers:

$score = $healthScorer->score($tenant);
$score->overallScore;  // 85
$score->status;        // 'healthy' (80+), 'warning' (50-79), 'critical' (<50)
$score->metrics;       // ['storage' => MetricScore, 'database' => MetricScore, ...]

// Custom health metric
class DatabaseHealthProvider implements HealthMetricProviderInterface
{
    public function measure(TenantInterface $tenant): MetricScore
    {
        return MetricScore::fromScore('database', $cacheHitRatio, 'Cache hit: 95%');
    }
}

Admin API

JSON API endpoints for tenant administration. Import the routing in your app:

# config/routes/tenancy_admin.yaml
tenancy_admin:
    resource: '@TenancyBundle/config/routes/admin_api.php'
    prefix: /tenancy/api

Endpoints: GET /tenants, GET /tenants/{id}, POST /tenants/{id}/suspend, POST /tenants/{id}/unsuspend, POST /tenants/{id}/maintenance.

Audit Logging

$auditLogger->log('user.login', 'User', $userId, ['ip' => $request->getClientIp()]);
$auditLogger->logEntityChange('Product', $productId, $changeSet);

Backup & Recovery

Implement BackupStrategyInterface or use the built-in PostgreSQLBackupStrategy:

$backupManager->backupTenant($tenant);
$backupManager->restoreTenant($tenant, '/path/to/backup.sql.gz');
$backupManager->listBackups($tenant);

Performance Tools

  • ConnectionPoolManager — connection reuse across tenant switches
  • QueryResultCache — tenant-aware query caching
  • DatabaseStatisticsCollector — schema size, row counts, cache hit ratio
  • IndexRecommendationService — slow query analysis
  • TenantArchivingService — archive/restore inactive tenants

Multi-Server / Cluster Support

Distribute tenants across multiple database servers:

// Implement ServerRegistryInterface for your topology
$server = $serverResolver->resolve($tenant); // auto-assigns to best server

// Dynamic connection routing
$params = $connectionFactory->getConnectionParams($tenant);  // primary
$params = $connectionFactory->getReadReplicaParams($tenant);  // read replica
tenancy:server:list                     # List servers with utilization

Monolog Integration

Automatic tenant context in all logs:

mon

Dependencies

Dependencies

ID Version
doctrine/dbal ^4.0
doctrine/doctrine-bundle ^2.12 || ^3.0
doctrine/doctrine-migrations-bundle ^3.0
doctrine/orm ^3.0
ext-openssl *
ext-zip *
php >=8.4
symfony/config ^7.3 || ^8.0
symfony/console ^7.3 || ^8.0
symfony/dependency-injection ^7.3 || ^8.0
symfony/event-dispatcher ^7.3 || ^8.0
symfony/framework-bundle ^7.3 || ^8.0
symfony/http-kernel ^7.3 || ^8.0
symfony/messenger ^7.3 || ^8.0

Development dependencies

ID Version
friendsofphp/php-cs-fixer ^3.92
monolog/monolog ^3.10
phpstan/phpstan ^2.0
phpunit/phpunit ^11.0 || ^12.0
symfony/cache ^7.3 || ^8.0
symfony/security-bundle ^7.3 || ^8.0
symfony/security-core ^7.3 || ^8.0
symfony/var-dumper ^7.3 || ^8.0
twig/twig ^3.0
Details
Composer
2026-03-17 12:13:05 +00:00
0
Customable
MIT
306 KiB
Assets (1)
Versions (39) View all
3.1.0 2026-03-18
3.0.20 2026-03-18
3.0.19 2026-03-18
3.0.18 2026-03-18
3.0.17 2026-03-18