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

Router Extra Bundle Laravel Package

jdecoster/router-extra-bundle

A Symfony bundle that extends the default router with additional routing features, helping you define richer route configurations and reduce boilerplate in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. jdecoster/router-extra-bundle is a Symfony bundle, not a Laravel package. There is no composer require path into a Laravel app that yields anything usable — no Flex recipe, no service provider, no facade. Its surface is Bundle subclasses, Kernel::registerBundles(), and the Symfony RouterInterface. If you're on Laravel, you do not install this; you read it as a reference and reproduce its three ideas with native route registration. Budget one hour for that, and treat any attempt at an adapter as a non-starter.

Where to look first. Skip composer.json and the README — read the routing loader. The entire value of the package lives in the small amount of code that iterates a RouteCollection and rewrites entries: renaming routes to apply a prefix, duplicating entries to create aliases, and attaching conditions. Everything else is bundle plumbing. If you can find that loop, you've extracted the whole package.

The three features, translated to Laravel

Bundle feature Laravel native equivalent
Route alias (one controller, many paths) Array of URIs on a single route definition
Route condition (match on headers/attributes) Route::domain(), ->where() constraints, or middleware
Name prefix Route::name('admin.')->group(...)
Annotation support PHP 8 attributes on controllers

First use case. Legacy URL preservation. You have /products/{slug} and want /p/{slug} to hit the same controller without a second named route:

Route::get(['/products/{slug}', '/p/{slug}'], [ProductController::class, 'show'])
    ->name('products.show');

That is the entire "route alias" feature, done natively, one line, and it stays compatible with route('products.show') and php artisan route:cache. Compare that against adding an unmaintained Symfony dependency and you have your answer on adoption.

If you genuinely are on a legacy Symfony 2.x/3.x app, register the bundle the traditional way — new JDecoster\RoutingExtraBundle\JDecosterRoutingExtraBundle() in AppKernel.php — and verify the container compiles before writing any YAML. But confirm the upstream symfony-cmf/routing-bundle doesn't already do what you need, because that fork is almost certainly downstream of it.

Implementation Patterns

Aliases as data, not as routes. The bundle's alias feature is static — one route definition, several URIs, fixed at deploy time. That covers renames and vanity URLs, but it does not cover slug changes over time (a product's slug changes weekly and the old one must keep resolving). For that, use a redirects table with a Route::fallback() handler or a middleware that checks a 404 against the table and 301s. Keeping aliases in config means every alias change is a deploy; keeping them in data means route:cache stops being a problem.

Name-prefix grouping. Laravel's ->name() group prefix is the direct equivalent and composes across nesting:

Route::prefix('admin')->name('admin.')->group(function () {
    Route::name('users.')->group(function () {
        Route::get('/users', [UserController::class, 'index'])->name('index');
        // → admin.users.index
    });
});

The bundle's variant exists because Symfony's import syntax historically lacked this. Laravel has never lacked it. Any time you're reaching for the bundle's prefix behavior, you're reaching for ->name() on a group.

Conditions → middleware and constraints. The bundle matches requests on headers and attributes at routing time. Laravel matches at dispatch time. That distinction matters: a middleware-level header check can only reject a request after a route has already matched, so two routes differing only by Accept header will collide. If you need genuine match-time discrimination, use what Laravel exposes at match time:

  • Route::domain('{tenant}.example.com') — host-based routing, the closest thing to a Symfony route condition.
  • ->where('slug', '[a-z0-9-]+') — parameter regex constraints.
  • Route::match(['get','post'], ...) — method predicates.

For tenant/locale/feature-flag conditions, the idiomatic pattern is a middleware group wrapped around the routes, not a per-route predicate:

Route::prefix('{locale}')
    ->whereIn('locale', config('app.locales'))
    ->middleware(SetLocale::class)
    ->group(base_path('routes/web.php'));

Dynamic route sets. This is where the bundle's lineage actually gets interesting — it descends from the Symfony CMF dynamic-routing work, where routes come from a database or CMS rather than code. In Laravel the cache-safe pattern is not to load routes from Eloquent at boot (that breaks route:cache), but to register one catch-all and resolve inside it:

Route::fallback(function (Request $request) {
    $page = Page::where('path', $request->path())->firstOrFail();
    return app()->call([PageController::class, 'show'], ['page' => $page]);
});

This preserves route caching, keeps the route table small, and pushes resolution cost to only the requests that miss every static route. If route cardinality is in the thousands and you need reverse routing (route('page', $page) to generate URLs), you'll want a URL generator binding on top — but that's a build, not an install.

Feature-flagged route files. Register route files conditionally in RouteServiceProvider::boot():

$this->routes(function () {
    Route::middleware('web')->group(base_path('routes/web.php'));
    if (config('features.beta')) {
        Route::middleware(['web', 'beta'])->group(base_path('routes/beta.php'));
    }
});

This is the pattern the bundle gestures at with condition-based route sets, and it's the one worth copying.

Registration hygiene. Load alias/redirect route files last, after all wildcard-bearing routes. Anything registered with a {slug} segment will shadow a later literal path.

Gotchas and Tips

Don't install it into a Laravel app. Not "probably don't" — don't. Even if Composer resolves the Symfony constraint tree, nothing in Laravel instantiates a bundle. You'll add ~20 transitive Symfony packages to your vendor dir for zero behavior change and a permanent upgrade tax.

Route caching kills anything dynamic. Any pattern that queries the database at boot time to register routes will either break php artisan route:cache outright or produce a stale cached route file that survives content changes and serves 404s. If you build the DB-backed loader, add explicit cache invalidation on write and test route:cache → change data → request in staging. This is the single most common failure mode in this problem space.

Aliases don't get their own names. A second URI on a route doesn't create a second route() helper target. If you need route('legacy.product') to keep generating old URLs, that's an explicit Route::redirect() with a 301 (good for SEO) or a separate route — not an alias.

Order matters more than you think. Route::get('/{slug}') registered before Route::get('/about') swallows the static page. Laravel matches in registration order; the bundle's condition feature exists partly to avoid this in Symfony. In Laravel your mitigations are registration order and constraints, not predicates.

Constraints are your condition escape hatch. ->where() is the only place Laravel exposes match-time logic that isn't path-based. Use ->whereIn() for enumerable values, and be aware a poorly-anchored regex will silently match everything.

Header conditions can't rescue a collision. Because middleware runs post-match, two routes differing only by a header will always resolve to whichever registered first. If you're porting Symfony condition logic to middleware, you're changing semantics, not translating them.

On the Symfony side (if you're actually there): the bundle predates PHP 8 attributes, so an annotation/attribute import won't pick up #[Route] — verify with a controller that has one and check whether the route appears at all. Also check whether the bundle's service is public before trying to pull it from the test container; private services fail in a way that looks like "bundle didn't register."

No extension points worth committing to. Single maintainer, one star, no dependents, no tests. There is nothing to extend and nobody to ask when it breaks. If you want the alias/prefix/condition logic, copy the idea — roughly 100 lines in a RouteServiceProvider you own — rather than vendoring Symfony-2-era code you'd have to keep alive.

Debugging tools you'll actually use. php artisan route:list -v (shows middleware and names), --path=admin to isolate a group, --except-vendor to cut noise, and php artisan route:clear immediately after editing route files because the cached route file is the usual suspect when "my change did nothing."

Keep any in-house equivalent behind config. Whether it's a redirect table or a dynamic loader, make it toggleable in config/features.php so that when the requirement evaporates — and it often does — you delete a branch instead of untangling routes from controllers.

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