geek-hub/domain-routing-bundle
Geek Hub Domain Routing Bundle adds domain-aware route registration to Laravel, letting you map routes to specific hostnames or subdomains. Configure domains and route groups to serve separate sections of your app from different domains without duplicating route definitions.
First, the honest orientation: geek-hub/domain-routing-bundle is a Symfony bundle, not a Laravel package. You cannot composer require it into a Laravel app and get working domain routing — it ships a Symfony Bundle, a DependencyInjection\Configuration tree, and a compiler pass, none of which Laravel's container wires up. For a Laravel developer, "leveraging" this package day-to-day means one of two things:
Given 0 stars, 0 dependents, and no maintainer signal, option 2 is the realistic default. But you still have to ship something today, so start here:
In a Symfony app:
composer require geek-hub/domain-routing-bundle
// config/bundles.php
return [
GeekHub\DomainRouting\DomainRoutingBundle::class => ['all' => true],
];
Confirm the PSR-4 namespace against vendor/geek-hub/domain-routing-bundle/composer.json before pasting — there is no community copy-paste to verify against.
In a Laravel app — skip the install entirely. Your first use case, expressed natively, is roughly:
// routes/web.php or RouteServiceProvider::boot()
foreach (config('domains.map') as $host => $group) {
Route::domain($host)
->middleware($group['middleware'] ?? 'web')
->group(base_path($group['routes']));
}
// config/domains.php
return [
'map' => [
'brand.de' => ['routes' => 'routes/domains/brand-de.php', 'middleware' => ['web', 'locale:de']],
'brand.fr' => ['routes' => 'routes/domains/brand-fr.php', 'middleware' => ['web', 'locale:fr']],
'{tenant}.app.com' => ['routes' => 'routes/domains/tenant.php', 'middleware' => ['web', 'tenant']],
],
];
Where to look first in the repo, in this order:
README.md — often the only spec.DependencyInjection/Configuration.php — the authoritative config key list; the README lags behind it.Routing/ or Loader/) — this is where the domain-match decision lives. That file is the one worth reading carefully, because it is the piece you'd reimplement.Canonical first use case: two regional domains (brand.de, brand.fr) that share one codebase but need different route sets and middleware (locale, currency, tax). That is the pattern the package is shaped around, and the first thing to reproduce natively if you go that route.
Pattern 1 — Config-driven domain map (the package's core idea). Keep a single config array mapping host → { route file, prefix, middleware, defaults }. Iterate that map once in the route service provider. Benefits over hand-written Route::domain()->group() blocks: the domain list becomes data (env-configurable, testable, diffable), not code. This is the one idea worth importing even if you never install the bundle.
Pattern 2 — Per-domain route files, isolated. routes/domains/brand-de.php, routes/domains/brand-fr.php. Never import a shared route file and a domain file in the same group unless the domain file re-declares its prefix — ordering matters and duplicate names will collide. Route-file isolation is what makes the domains independently cacheable and independently testable.
Pattern 3 — Per-domain middleware stacks. Encode locale/currency/auth-scope in the domain map, not in individual route definitions:
'brand.fr' => [
'routes' => 'routes/domains/brand-fr.php',
'middleware' => ['web', 'set-locale:fr', 'currency:EUR'],
],
This is the highest-leverage part of the pattern — it means adding a domain is a config edit plus a route file, not a sweep through controllers.
Pattern 4 — Wildcard subdomains with parameter binding. Both the bundle and native Laravel support {subdomain} host segments. In Laravel, bind the parameter in middleware (ResolveTenant), not in route closures — route closures get cached and closure-based tenant resolution will bite you on route:cache. In Symfony, the equivalent is a request listener that reads the host and hydrates the tenant context before RouterListener dispatches.
Pattern 5 — Explicit default/fallback domain. Always define what happens for an unknown host, and make it explicit rather than incidental. The safe default is a 404 or a redirect to a canonical domain — never a silent fallthrough to a "first" or "default" tenant. Silent fallback is a cross-tenant data exposure waiting to happen. Write a test named after this behavior so nobody "simplifies" it later.
Pattern 6 — Route caching as a first-class workflow step. In Laravel, php artisan route:cache is the sharp edge: a config-driven loop must generate stable route names or the cache silently drops domains. In Symfony, equivalent concerns apply to the compiled router. Make a route-cache assertion part of CI from day one:
// tests/Feature/RouteCacheTest.php
$this->artisan('route:cache')->assertExitCode(0);
$this->get('https://brand.de/')->assertOk();
$this->get('https://brand.fr/')->assertOk();
If this test does not exist, domain routing is not actually shipped — it is just working on your laptop.
Pattern 7 — Use it as a schema reference. Read Configuration.php, lift the shape (domain => [routes, prefix, middleware]), and mirror it into native route groups. Time-box this to half a day. That is the highest-value use of this repo for a Laravel team, and it costs nothing in dependency risk.
Framework mismatch is the first gotcha, and it is fatal in Laravel. Installing this bundle into a Laravel app gives you dead autoloaded weight and no wiring. If someone on the team added it "to try," remove it — it will confuse the next developer who greps for domain routing and finds an unused Symfony bundle in composer.json.
Zero adoption means zero search results. When it breaks, there is no Stack Overflow answer, no GitHub issue thread, no changelog to blame. Budget for first-principles debugging. Keep the repo vendored or forked with a pinned commit if you actually depend on it in production.
The Host header is untrusted input. Domain routing is a security-relevant surface: an attacker-controlled Host header drives which routes and which middleware apply. Mitigations: explicit host allowlist (derived from config, not a regex over the raw header), Vary: Host on every cached response, and a hard 404 on unknown hosts. If you use a CDN or shared cache, a missing Vary: Host turns a config typo into cross-tenant cache poisoning.
Config cache staleness is the most common operational bug. Adding a domain without clearing and rebuilding the config/route cache produces the worst failure mode: routes exist in the file, never match at runtime, and there is no error. In Laravel this is route:clear && route:cache; in Symfony it is cache:clear plus a router rebuild. Add it to your deploy script and to your runbook, explicitly.
Precedence when multiple domains match. If you have brand.de and a wildcard {tenant}.brand.de, the order of registration decides the winner. Define the precedence rule once (most-specific-first), sort the config map accordingly, and write a test for the overlap case. Do not rely on config file ordering — it will be reordered by a merge someday.
Do not fork the bundle to "fix" it for Laravel. A fork inherits the Symfony coupling without inheriting any upstream maintenance. If you need the capability in Laravel, write the 20-line RouteServiceProvider loop. If you need it in Symfony, fork and assign a named owner and a patch cadence — otherwise you have adopted a dependency with a bus factor of one and then become that one.
Extension point worth knowing: the bundle's domain resolution hooks into Symfony's RequestContext / RouterListener lifecycle. This is why it cannot be ported into Laravel as-is — Laravel wraps Symfony Routing but does not expose the bundle extension points needed to hook a third-party bundle into request dispatch. Do not spend time trying to bridge it; that is the architectural gap, and it is not a small one.
Upgrade pressure is the highest-probability failure mode. An unmaintained package pinned into the request lifecycle of a Symfony app becomes a blocked framework upgrade, which becomes an emergency rewrite under deadline. If you adopt it in Symfony, note the pinned Symfony minor version in an ADR and revisit the decision at every framework upgrade. If you adopt the pattern in Laravel, you have none of this — your routing rides on framework upgrades the team is already doing.
Recommendation, stated plainly: adopt the pattern, decline the package in Laravel, and treat any Symfony adoption as a time-boxed spike with a named owner. The capability is one to two days of native work; the dependency is open-ended risk on a request-path security surface.
How can I help you explore Laravel packages today?