netgusto/dynamic-subdomain-bundle
Symfony bundle that brings dynamic subdomain routing to your app: define routes on wildcard hosts like {tenant}.example.com, match incoming subdomains automatically, and generate subdomain-aware URLs with minimal configuration.
This is a Symfony2-era bundle, not a Laravel package. There is no service provider, no facade, and no config/*.php merge. If you are evaluating it for a Laravel codebase, treat it as a reference implementation of "subdomain → tenant routing" rather than something you will composer require.
Minimal steps in its native Symfony2 context:
composer require netgusto/dynamic-subdomain-bundle (old constraints — expect PHP 5.x/7.0-era platform requirements; you will likely need --ignore-platform-reqs or a legacy PHP container).app/AppKernel.php:
$bundles = [
// ...
new Netgusto\DynamicSubdomainBundle\NetgustoDynamicSubdomainBundle(),
];
app/config/config.yml (the README is the only reliable source of the exact keys — the bundle is small enough to read its DependencyInjection/Configuration.php).Resources/config/services.xml first. That file tells you what the bundle actually wires up — usually a request listener and/or a routing loader — faster than any docs.*.example.com) at your dev box, define a route whose host contains a placeholder, and confirm the value lands in $request->attributes inside a controller.The practical "first use case" is a single-tenant-to-multi-tenant pivot: acme.example.com should resolve to the same controller as globex.example.com but with a tenant identifier already parsed out of the host before your controller runs.
Route-level host matching (the pattern actually worth stealing). Symfony2's router has supported host placeholders since 2.2, so most implementations boil down to:
tenant_dashboard:
path: /dashboard
host: "{tenant}.example.com"
defaults: { _controller: AppBundle:Dashboard:index, tenant: null }
requirements: { tenant: "[a-z0-9-]+" }
The bundle's contribution is typically the listener that normalizes the host into a request attribute before routing, so controllers never touch $request->getHost() directly. In Laravel the direct equivalent is:
Route::domain('{tenant}.example.com')->group(function () {
Route::get('/dashboard', DashboardController::class);
});
If you are porting, that one-liner in routes/web.php replaces most of what this bundle exists for.
Middleware over listeners. Whatever the bundle does in a Symfony kernel.request listener, the Laravel translation is a middleware that resolves the tenant and either binds it into the container or aborts early:
public function handle($request, Closure $next)
{
$host = $request->getHost();
$parts = explode('.', $host);
$sub = count($parts) > 2 ? $parts[0] : null;
abort_unless(Tenant::where('slug', $sub)->exists(), 404);
app()->instance(Tenant::class, Tenant::where('slug', $sub)->firstOrFail());
return $next($request);
}
Register it globally after SubstituteBindings so route-model binding sees the tenant, and scope it to tenant routes only to avoid running tenant lookups on your marketing site or health checks.
Environment-sensitive host parsing. The classic integration bug: localhost has no subdomain, acme.localhost has one, and acme.example.test:8000 has a port. Parse defensively (parse_url on a synthesized scheme, or Str::before($host, '.') guarded by a dot count), and keep a config flag for the base domain so a future domain migration does not mean grepping for string literals.
Pair with a tenant-aware connection resolver. In the Symfony world this bundle is often the front half of a multi-database setup. In Laravel, put the tenant switching in the same middleware (config(['database.connections.tenant.database' => $tenant->db]) then DB::purge('tenant')), so resolution and connection selection are one atomic step. Splitting them across two middlewares is how you end up querying the wrong database under Octane.
It is effectively unmaintained. Symfony2 only, 14 stars, zero dependents, last activity from the Symfony 2.x era. Do not build a new product on it. Its value today is as a 200-line reference for how host-placeholder routing was done before frameworks made it a first-class feature. Laravel has had Route::domain() for years; use that.
Wildcard DNS is a prerequisite, not a detail. The bundle cannot conjure subdomains. You need *.example.com A <ip> (or a wildcard on your load balancer) plus matching TLS. Symmetric wildcard certs (*.example.com) cover one label only — a.b.example.com will fail TLS. Test with dig before debugging PHP.
Host vs. subdomain confusion. $request->getHost() excludes the port but includes every label. Behind a proxy or Heroku-style router, the Host header may be rewritten — set trusted proxies (Request::setTrustedProxies) or your subdomain parsing silently sees the internal hostname. This is the number one reason "it works locally but not in staging."
Route caching will bite you. Symfony compiled routes and Laravel's route:cache both freeze host patterns. Clear caches after editing wildcard rules; a stale cache produces a 404 that looks like a DNS problem.
Subdomain enumeration is an attack surface. If an unknown subdomain falls through to a 404 that says "tenant not found," you have a tenant-enumeration oracle. Return a generic 404, and rate-limit tenant resolution lookups — this bundle predates that concern entirely.
Extension point. The bundle's real hook is its request listener; if you must keep it alive, decorate that service rather than forking. Look for a service ID like netgusto.dynamic_subdomain.listener and override it in services.yml with a decorates: key.
Debugging shortcut. When routing "mysteriously" fails on a subdomain, dump $request->getHost() and $request->attributes->all() at the top of kernel.request (priority > 32 so it runs before the router). Ninety percent of issues are a proxy-rewritten Host header or a requirement regex rejecting uppercase labels ([a-z0-9-]+ will not match ACME — normalize with strtolower first).
Migration advice for Laravel teams: skip the bundle. You need three pieces — Route::domain(), a tenant-resolution middleware, and a per-tenant DB/cache prefix — and all three are first-party. Budget half a day, not a legacy-container spelunking expedition.
How can I help you explore Laravel packages today?