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

Tenancy Bundle Laravel Package

danplaton4/tenancy-bundle

Multi-tenancy for Symfony with zero boilerplate: resolve a tenant once per request and the kernel reconfigures DBAL/Doctrine, cache pools, mailer transport, and Messenger. Automatic query scoping and tenant propagation to workers; your app code stays tenant-unaware.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation

    composer require danplaton4/tenancy-bundle
    

    Run the one-shot setup:

    composer require --dev nikic/php-parser  # Required for auto-config
    bin/console tenancy:install
    
  2. Configure Tenancy Edit config/packages/tenancy.yaml:

    tenancy:
        driver: database_per_tenant  # or shared_db
        database:
            enabled: true
    
  3. First Use Case

    • For database-per-tenant, create a tenant entity (e.g., Tenant with slug as primary key).
    • For shared-db, mark entities with #[TenantAware]:
      use Tenancy\Bundle\Attribute\TenantAware;
      
      #[ORM\Entity]
      #[TenantAware]
      class Invoice {}
      
  4. Test Resolution Access your app via a subdomain (e.g., tenant1.your-app.local) or set the X-Tenant-ID header to trigger resolution.


Implementation Patterns

Tenant Resolution Workflow

  1. Resolver Chain Configure resolvers in tenancy.yaml (e.g., host, header, query_param):

    tenancy:
        resolvers:
            - host
            - header
            - query_param
    
    • Priority Order: Lower numbers resolve first (e.g., query_param at 10 has highest priority).
    • Custom Resolvers: Implement TenantResolverInterface and tag with tenancy.resolver.
  2. Database-Per-Tenant

    • Connection Switching: The DatabaseSwitchBootstrapper automatically switches the DBAL connection to the tenant’s database.
    • Migrations: Run per-tenant migrations with:
      bin/console tenancy:migrate
      
  3. Shared-DB Mode

    • SQL Filtering: Entities marked with #[TenantAware] are automatically scoped to the active tenant.
    • Strict Mode: Throws TenantMissingException if no tenant is resolved (default behavior).
  4. Bootstrappers

    • Built-in Bootstrappers:
      • DoctrineBootstrapper: Manages Doctrine entity managers.
      • CacheBootstrapper: Isolates cache pools by tenant.
      • MailerBootstrapper: Swaps SMTP/DSN per tenant.
      • MessengerBootstrapper: Propagates tenant context via TenantStamp.
    • Custom Bootstrappers: Implement TenantBootstrapperInterface and tag with tenancy.bootstrapper:
      use Tenancy\Bundle\Bootstrapper\TenantBootstrapperInterface;
      
      class CustomBootstrapper implements TenantBootstrapperInterface {
          public function boot(TenantContext $context): void {
              // Reconfigure your subsystem here
          }
      }
      
  5. CLI Tenant Context Run commands with a tenant context:

    bin/console tenancy:run --tenant=acme doctrine:migrations:migrate
    
  6. Testing Use the InteractsWithTenancy trait in PHPUnit:

    use Tenancy\Bundle\Test\InteractsWithTenancy;
    
    class InvoiceTest extends TestCase {
        use InteractsWithTenancy;
    
        public function testInvoiceCreation() {
            $this->setActiveTenant('acme');
            // Test logic here
        }
    }
    

Gotchas and Tips

Pitfalls

  1. Resolver Conflicts

    • If multiple resolvers match (e.g., host and header), the lowest-priority resolver wins (e.g., host at 30 overrides header at 20).
    • Fix: Adjust priorities in tenancy.yaml or use a custom resolver with higher priority.
  2. Shared-DB + Database-Per-Tenant Conflict

    • Configuring shared_db with database.enabled: true throws a validation error.
    • Fix: Ensure database.enabled: false in shared_db mode.
  3. Cache Isolation Leaks

    • If using non-isolated cache pools (e.g., Redis without namespacing), tenant data may leak.
    • Fix: Always use the CacheBootstrapper or configure per-tenant cache prefixes manually.
  4. Messenger Transport Issues

    • Async transports (e.g., doctrine) may not propagate tenant context if not configured.
    • Fix: Ensure MessengerBootstrapper is enabled and TenantStamp is attached to envelopes.
  5. Doctrine Identity Map Pollution

    • Shared entity managers across tenants can cause stale data.
    • Fix: Use EntityManagerResetListener (enabled by default) or reset the manager manually:
      $this->getDoctrine()->getManager()->clear();
      
  6. Strict Mode Pitfalls

    • Strict mode throws TenantMissingException if no tenant is resolved.
    • Fix: Opt out in tenancy.yaml:
      tenancy:
          strict_mode: false
      

Debugging Tips

  1. Profiler Tab

    • Enable the Symfony Profiler (kernel.debug: true) to inspect the active tenant, resolver, and bootstrappers in the "Tenancy" panel.
  2. Logging Tenant Context

    • Log the active tenant in a kernel.request listener:
      use Tenancy\Bundle\Context\TenantContext;
      
      public function onKernelRequest(RequestEvent $event, TenantContext $context) {
          $this->logger->info('Active tenant:', ['tenant' => $context->getTenant()]);
      }
      
  3. Testing Resolvers

    • Mock the Request object to test resolver behavior:
      $request = new Request([], [], ['HTTP_X_TENANT_ID' => 'acme']);
      $this->setActiveTenantFromRequest($request);
      
  4. Database Connection Issues

    • Verify tenant databases exist and are accessible. Use:
      bin/console tenancy:run --tenant=acme doctrine:schema:validate
      

Extension Points

  1. Custom Tenant Entity Extend AbstractTenant to add custom fields:

    use Tenancy\Bundle\Entity\AbstractTenant;
    
    #[ORM\Entity]
    class CustomTenant extends AbstractTenant {
        #[ORM\Column]
        private string $brandColor = '#000000';
    }
    
  2. Dynamic Bootstrappers Conditionally enable bootstrappers based on tenant attributes:

    class DynamicMailerBootstrapper implements TenantBootstrapperInterface {
        public function boot(TenantContext $context): void {
            if ($context->getTenant()->isPremium()) {
                $this->mailer->configurePremiumTransport();
            }
        }
    }
    
  3. Resolver Middleware Add middleware to inject tenant context early (e.g., for API gateways):

    use Tenancy\Bundle\Resolver\TenantResolverInterface;
    
    class TenantMiddleware implements MiddlewareInterface {
        public function __construct(private TenantResolverInterface $resolver) {}
    
        public function handle(Request $request, callable $next): Response {
            $this->resolver->resolveFromRequest($request);
            return $next($request);
        }
    }
    
  4. Override Default Bootstrappers Replace built-in bootstrappers by implementing TenantBootstrapperInterface and overriding the service tag:

    services:
        Tenancy\Bundle\Bootstrapper\DoctrineBootstrapper:
            class: App\Bootstrapper\CustomDoctrineBootstrapper
            tags: ['tenancy.bootstrapper']
    
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.
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor