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.
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:
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.*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.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.
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.
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.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.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.tenant:debug {id}) that prints resolved config, connection name, cache prefix, and storage root pays for itself within a week.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.How can I help you explore Laravel packages today?