Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Multi Tenancy Bundle Laravel Package

apajo/symfony-multi-tenancy-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Bundle

    composer require apajo/symfony-multi-tenancy-bundle
    

    Enable in config/bundles.php:

    aPajo\MultiTenancyBundle\APajoMultiTenancyBundle::class => ['all' => true],
    
  2. Configure Doctrine Define two connections (default and tenant) and entity managers in config/packages/doctrine.yaml:

    doctrine:
      dbal:
        connections:
          default: { url: '%env(DEFAULT_DATABASE_URL)%' }
          tenant:  { url: '%env(TENANT_DATABASE_URL)%' }
      orm:
        entity_managers:
          default: { connection: default }
          tenant:  { connection: tenant, mappings: [...] }
    
  3. Configure Tenant & Adapters Define tenant entity, adapters, and resolvers in config/packages/apajo_multi_tenancy.yaml:

    apajo_multi_tenancy:
      tenant:
        class: App\Entity\Tenant
        identifier: key
        entity_manager: default
        resolvers:
          - aPajo\MultiTenancyBundle\Service\Resolver\HostBasedResolver
      adapters:
        - aPajo\MultiTenancyBundle\Adapter\Database\DatabaseAdapter
        - aPajo\MultiTenancyBundle\Adapter\Filesystem\FilesystemAdapter
    
  4. Register Adapters as Services Example for FilesystemAdapter in config/services.yaml:

    services:
      aPajo\MultiTenancyBundle\Adapter\Filesystem\FilesystemAdapter:
        arguments: ['@sonata.media.adapter.filesystem.local']
    
  5. First Tenant Switch Use the EnvironmentProvider to switch tenants dynamically:

    use aPajo\MultiTenancyBundle\Service\EnvironmentProvider;
    
    $tenant = $tenantRepository->find('tenant_key');
    $environmentProvider->select($tenant);
    

Implementation Patterns

Core Workflow: Tenant-Aware Operations

  1. Resolver Integration Use built-in resolvers (e.g., HostBasedResolver) to auto-detect tenants via:

    • Subdomains (tenant.example.com)
    • Headers (X-Tenant-ID)
    • Custom logic (extend TenantResolverInterface).
  2. Adapter-Based Configuration Dynamically override system configurations per tenant:

    // FilesystemAdapter: Switch storage paths
    $adapter = $container->get(FilesystemAdapter::class);
    $adapter->setTenantConfig($tenant);
    
    // MailerAdapter: Override transport per tenant
    $mailerAdapter = $container->get(MailerAdapter::class);
    $mailerAdapter->setTenantConfig($tenant);
    
  3. Entity Manager Context Use the tenant entity manager for tenant-specific data:

    $tenantRepo = $entityManager->getRepository('App\Entity\Tenant');
    $tenantRepo->find('key'); // Uses tenant EM
    
  4. Migrations

    • Generate tenant-specific migrations:
      php bin/console tenants:migrations:diff
      
    • Apply migrations to all tenants or a single tenant:
      php bin/console tenants:migrations:migrate [tenant_id]
      

Common Patterns

  • Tenant-Specific Services Create services that inject EnvironmentProvider to access tenant configs:

    class TenantAwareService {
        public function __construct(private EnvironmentProvider $envProvider) {}
    
        public function getTenantConfig() {
            return $this->envProvider->getCurrentTenant()->getConfig();
        }
    }
    
  • Middleware for Tenant Resolution Use Symfony middleware to resolve tenants early in the request lifecycle:

    use aPajo\MultiTenancyBundle\Service\Resolver\TenantResolverInterface;
    
    class TenantMiddleware implements MiddlewareInterface {
        public function __construct(private TenantResolverInterface $resolver) {}
    
        public function process(Request $request, RequestHandler $handler): Response {
            $tenant = $this->resolver->resolve($request);
            $envProvider->select($tenant);
            return $handler->handle($request);
        }
    }
    
  • Event-Driven Tenant Switching Dispatch TenantSelectEvent to trigger tenant-specific logic:

    $event = new TenantSelectEvent($tenant);
    $dispatcher->dispatch($event);
    

Gotchas and Tips

Pitfalls

  1. Default Connection Dependency

    • Some bundles (e.g., Symfony Security) may hardcode the default connection.
    • Fix: Ensure default connection is always present, even if unused.
  2. Entity Manager Context Leaks

    • Avoid mixing default and tenant entity managers in the same transaction.
    • Fix: Use tenant EM exclusively for tenant-specific operations.
  3. Resolver Conflicts

    • Multiple resolvers may override each other unpredictably.
    • Fix: Order resolvers by priority in apajo_multi_tenancy.yml or use a custom resolver.
  4. Migration Gaps

    • Tenant migrations may fail if the tenant connection is misconfigured.
    • Fix: Test migrations with tenants:migrations:diff before applying.
  5. Adapter Initialization Order

    • Adapters must be initialized after the tenant is resolved.
    • Fix: Use TenantSelectEvent listeners to configure adapters.

Debugging Tips

  • Check Current Tenant Log the active tenant in middleware or controllers:

    $tenant = $envProvider->getCurrentTenant();
    error_log("Active Tenant: " . $tenant->getKey());
    
  • Profiler Limitations Symfony Profiler may not show tenant-specific data. Use custom data collectors:

    class TenantDataCollector implements DataCollectorInterface {
        public function collect(Request $request, Response $response, \Exception $exception = null) {
            return ['tenant' => $envProvider->getCurrentTenant()];
        }
    }
    
  • Adapter Debugging Enable adapter logging to trace config changes:

    services:
      aPajo\MultiTenancyBundle\Adapter\Database\DatabaseAdapter:
        arguments: ['@doctrine']
        calls:
          - [setDebug, [true]]
    

Extension Points

  1. Custom Adapters Extend AdapterInterface to support new services (e.g., caching, queues):

    class CacheAdapter implements AdapterInterface {
        public function apply(TenantInterface $tenant) {
            $cache = new RedisCache($tenant->getCacheConfig());
            // Inject into container
        }
    }
    
  2. Custom Resolvers Implement TenantResolverInterface for unique tenant resolution logic:

    class ApiKeyResolver implements TenantResolverInterface {
        public function resolve(Request $request) {
            $key = $request->headers->get('X-API-KEY');
            return $tenantRepo->findOneBy(['api_key' => $key]);
        }
    }
    
  3. Tenant-Specific Doctrine Events Listen for tenant-specific ORM events:

    $tenantEM->getEventManager()->addEventListener(
        LifecycleEventArgs::class,
        function ($event) use ($tenant) {
            $entity = $event->getObject();
            if ($entity instanceof TenantAwareEntity) {
                $entity->setTenant($tenant);
            }
        }
    );
    
  4. Fallback Tenant Handle cases where no tenant is resolved (e.g., admin panel):

    $envProvider->select($fallbackTenant);
    

Configuration Quirks

  • Environment Variables Use %env(TENANT_DATABASE_URL)% with placeholders for dynamic tenant DB URLs:

    tenant:
      url: '%env(TENANT_DATABASE_URL_%tenant_key)%'
    
  • Adapter Prioritization Adapters are applied in the order defined in apajo_multi_tenancy.yml. Reorder as needed.

  • Migration Paths Ensure migrations/tenant/ and migrations/default/ directories exist and are writable.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
daikazu/eloquent-salesforce-objects
unseen-codes/chat
romalytar/yammi-jobs-monitoring-laravel
kisame76/filament-db-table-state
nqxcode/laravel-lucene-search
dpfx/laravel-livewire-wizards
workos/workos-php-laravel
sofa/laravel-global-scope
nawasara/auth-primitives
adhocrat-io/arkhe-main
make-dev/orca-harpoon
itsemon245/lamet
baks-dev/dashboard
amoifr/pickle-panther-bundle
make-dev/orca
dmstr/symfony-system-resources-bundle
dmstr/symfony-job-queue-bundle
dmstr/openapi-json-schema-bundle
dmstr/keycloak-security-bundle
dmstr/doctrine-audit-log-bundle