Weave 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

Tenant Aware Bundle Laravel Package

funkymed/tenant-aware-bundle

Symfony bundle for multi-tenant applications that makes Doctrine entities tenant-aware. It handles tenant context and automatic query filtering so each tenant sees only its own data, reducing boilerplate and helping keep tenant data isolated.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the framing: funkymed/tenant-aware-bundle is a Symfony bundle, not a Laravel package. You cannot composer require it into a Laravel app and expect a service provider to appear. Treat this assessment as an evaluation of a reference implementation of tenant-scoped configuration — the patterns are directly transferable, the code is not. If you're the Laravel dev on the team, your job is usually to answer "does this solve our problem, and should we port the idea or reach for a Laravel-native package (e.g. stancl/tenancy)?"

Minimal steps to evaluate it:

  1. Read the README for the intended tenancy axis. Bundles in this space usually assume one of: request host/subdomain, a header, an authenticated user's tenant claim, or an explicit CLI/env selector. That single assumption determines whether it fits your product at all.
  2. Read DependencyInjection/Configuration.php. In any Symfony bundle this file is the specification — the config tree tells you every knob the package actually supports, more reliably than the docs.
  3. Read the bundle class and any compiler passes (*Bundle.php, DependencyInjection/Compiler/*). This is where you'll see whether tenant config is resolved at container compile time (baked, per-environment) or at runtime (per-request). Almost every design consequence flows from that one choice.
  4. Find the tenant context service (something like a TenantContext, TenantResolver, or TenantStorage). That object is the concept you'd reimplement in Laravel.

Best first use case to test against: the "same code, per-tenant credentials and flags" scenario — different DB DSNs, mailer sender addresses, feature toggles, and third-party API keys per tenant, without duplicating .env files per customer. If the bundle's config tree can't express that cleanly, it's probably not worth porting.

Implementation Patterns

Pattern 1 — Read the config tree as a schema, then map it to Laravel config files. Symfony's Configuration class typically yields something like a tenant_aware: root with resolver, tenants, and per-tenant parameters. In Laravel you'd mirror this as config/tenant.php returning a resolver class name plus a tenants map, and you'd keep secrets out of it — reference env vars or a DB table instead.

Pattern 2 — Resolve once per request, in middleware, not in service constructors. Symfony bundles of this shape usually hang a kernel.request listener. The Laravel equivalent is a middleware that identifies the tenant and stamps it into a scoped container binding:

// app/Http/Middleware/ResolveTenant.php
public function handle($request, Closure $next)
{
    $tenant = $this->resolver->fromRequest($request); // host, header, or user claim
    abort_unless($tenant, 404);

    app()->instance(Tenant::class, $tenant);
    app()->instance('tenant.id', $tenant->id);

    // overlay tenant config on top of the base config
    foreach ($tenant->config as $key => $value) {
        config([$key => $value]);
    }

    return $next($request);
}

Pattern 3 — Build tenant-scoped services lazily via a factory, mirroring Symfony's service factories/decorators. The bundle's likely contribution is a factory that constructs a service (mailer, storage client, DB connection) from the resolved tenant. In Laravel, register these as scoped bindings so they're rebuilt per request and destroyed with it:

$this->app->scoped(Mailer::class, fn ($app) => $app->make(MailerFactory::class)
    ->forTenant($app->make(Tenant::class)));

Pattern 4 — Runtime DB connection switching. If the bundle swaps a Doctrine connection per tenant, the Laravel analogue is defining a connection at runtime before first use:

config(['database.connections.tenant' => [
    'driver' => 'mysql',
    'host' => $tenant->db_host,
    'database' => $tenant->db_name,
    'username' => $tenant->db_user,
    'password' => $tenant->db_password,
]]);
DB::purge('tenant');

Purge is non-negotiable — without it Laravel hands you the connection it already cached.

Pattern 5 — Propagate tenant context into async work. Symfony bundles often do this with a kernel.terminate hook or a messenger middleware. In Laravel, the equivalents are a job middleware / Queue::createPayloadUsing that serializes the tenant id, and a Bus::pipeThrough that re-hydrates it inside the worker. This is where most hand-rolled tenancy implementations quietly break.

Pattern 6 — Cache and filesystem keying. Any tenant-aware config bundle implies tenant-partitioned state. Prefix the cache (config(['cache.prefix' => "t{$tenant->id}:"])), and namespace the disk root ("tenants/{$tenant->id}"). Do this at the same layer as the config overlay so the two can't drift apart.

Integration tips: keep the resolver behind an interface so tests can bind a fake tenant; add a Tenant::fake()-style helper early; write one feature test that boots two tenants in the same process and asserts config isolation — that test will catch more regressions than any other.

Gotchas and Tips

  • The compile-time vs. runtime split is the whole ballgame. Symfony caches the compiled container; if the bundle resolves tenant config at compile time, per-tenant values must be enumerated in config, and you cannot invent a tenant at runtime. The Laravel parallel is php artisan config:cache: after caching, env() calls return null, so a resolver that reads env('TENANT_...') works in dev and silently dies in production. Read env only inside config/*.php, and read tenant values only through the resolver.
  • Long-running processes leak tenant state. Symfony's answer is usually a request-scoped service; Laravel's is Octane plus app()->scoped(). Any static property, singleton, or config() mutation survives into the next tenant's request. If you use Octane, add a RequestTerminated/OperationTerminated hook that flushes the tenant binding and re-prefixes the cache. Without it you will ship one tenant's data to another, and it will look like a caching bug.
  • Queue workers and scheduled commands have no request. The bundle likely assumes an HTTP entry point. Your jobs will run with whatever tenant context the last job left behind. Always serialize the tenant id into the payload and resolve it at handle() entry, and fail loudly if the id is missing rather than defaulting.
  • config() overlays are not reversible. Once you config(['services.x' => ...]), the original value is gone for the rest of the process. Snapshot and restore, or (better) read tenant values through a dedicated TenantConfig reader rather than mutating the global repository.
  • Debugging: when values look wrong, dump the resolved tenant and the overlay diff rather than the raw config. A tiny artisan command (tenant:debug {id}) that prints resolved config, connection name, cache prefix, and storage root pays for itself within a week.
  • Extension points worth stealing, even if you don't port the code: the resolver interface (swap host-based for header-based without touching consumers), tagged service registration for tenant-scoped services, and a compiler pass that validates config at boot rather than at first request.
  • Maturity risk. 27 stars and zero dependents means effectively one maintainer and no downstream signal. Pin the version, read the resolver and any DB-switching code line by line before trusting it with credentials, and budget for the fork to go unmaintained. For a Laravel codebase I'd port the pattern (roughly 150–300 lines of middleware, a resolver, scoped bindings, and job middleware) rather than depend on a cross-framework port — you'd spend more time bridging Symfony's DI into Laravel than writing the thing.
  • Don't forget the boring guards: a tenants table with a unique index on the host, a middleware that 404s on unknown hosts instead of falling back to a default tenant, and a hard block on config('app.debug') leaking stack traces that reveal other tenants' hostnames.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package