customable/tenancy-bundle (2.8.0)

Published 2026-03-14 18:42:21 +00:00 by jack

Installation

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

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]
    private ?int $id = null;

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

    public function getTenantIdentifier(): string { return $this->key; }
    public function getTenantKey(): string|int { return $this->id ?? 0; }
    public function getTenantSchema(): string { return 'tenant_' . $this->key; }
    public function getTenantData(): array { return []; }
}

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->getTenantIdentifier()}!");
}

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

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

# Migrations (auto-tagged with #[AsCentralMigration] / #[AsTenantMigration])
tenancy:make:migration              # tenant migration (default)
tenancy:make:migration central      # central migration
tenancy:make:migration --empty      # empty migration template
tenancy:migrate all
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

# 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);

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);

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

Monolog Integration

Automatic tenant context in all logs:

monolog:
    handlers:
        main:
            processors:
                - Customable\TenancyBundle\Logging\TenantContextProcessor

Feature Gates

Restrict routes and controllers to tenants with specific features enabled:

use Customable\TenancyBundle\Attribute\RequiresFeature;

#[Route('/api/v2')]
#[RequiresFeature('apiAccess')]
public function apiEndpoint(): Response { ... }

#[Route('/sso/login')]
#[RequiresFeature('ssoEnabled')]
public function ssoLogin(): Response { ... }

// Class-level: applies to all methods
#[RequiresFeature('apiAccess')]
class ApiController { ... }

Throws FeatureNotAvailableException (403) if the tenant's feature is disabled. Works with all boolean properties on TenantFeatures (apiAccess, ssoEnabled, customDomain).

Configuration Validation

Automatic validation of tenant configuration on creation, with built-in validators and a console command:

// Built-in validators:
// - FeatureLimitsValidator: maxUsers > 0, storageQuotaMb >= 0
// - MailConfigValidator: valid email addresses, DSN format
// - SchemaNameValidator: safe PostgreSQL schema names

// Custom validator:
use Customable\TenancyBundle\Validation\TenantConfigValidatorInterface;
use Customable\TenancyBundle\Validation\ValidationError;

class MyCustomValidator implements TenantConfigValidatorInterface
{
    public function validate(TenantInterface $tenant): array
    {
        $errors = [];
        // Your validation logic...
        return $errors;
    }
}
# Validate a single tenant
tenancy:config:validate acme

# Validate all tenants
tenancy:config:validate --all

Validation runs automatically on TenantCreatingEvent and throws InvalidTenantConfigException on failure.

Event System

39 lifecycle events for extensibility:

// Listen to tenant initialization
#[AsEventListener]
public function onTenancyInitialized(TenancyInitializedEvent $event): void
{
    $tenant = $event->tenant;
}

Key events: TenancyInitializing/Initialized, TenancyEnding/Ended, TenantCreating/Created, DatabaseMigrating/Migrated, BackupStarted/Completed, TenantSuspended/Unsuspended.

Messenger Integration

Tenant context is automatically carried through async messages:

framework:
    messenger:
        buses:
            messenger.bus.default:
                middleware:
                    - Customable\TenancyBundle\Messenger\Middleware\AddTenantStampMiddleware
                    - Customable\TenancyBundle\Messenger\Middleware\ResolveTenantMiddleware

Configuration Reference

tenancy:
    tenant_class: App\Entity\Tenant     # Required
    tenant_repository: ~                 # Auto-detected if null

    strategy:
        type: schema                     # schema, database, column
        prefix: tenant_

    connections:
        central: default
        tenant: default

    identification:
        hook: kernel.request             # kernel.request, routing, middleware
        resolvers: []                    # Auto-discovered via tags
        central_domains: []
        header_name: X-Tenant
        path_prefix: tenant
        query_parameter: tenant
        excluded_paths: [/health, /_wdt, /_profiler]

    bootstrappers:
        database: true
        cache: true
        filesystem: true
        session: false
        cookie: false
        filesystem_base_path: tenants
        cache_prefix: tenant_

    migrations:
        central_path: '%kernel.project_dir%/migrations/central'
        tenant_path: '%kernel.project_dir%/migrations/tenant'

    messenger:
        enabled: true

    routing:
        enabled: true
        scheme: https
        domain_pattern: '{tenant}.localhost'
        fallback_domain: localhost
        twig_extension: true

    performance:
        connection_pooling:
            enabled: false
            min_size: 5
            max_size: 50
        query_cache:
            enabled: false
            default_t

Dependencies

Dependencies

ID Version
doctrine/dbal ^4.0
doctrine/doctrine-bundle ^2.12 || ^3.0
doctrine/orm ^3.0
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-14 18:42:21 +00:00
0
Customable
MIT
239 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