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

Routing Laravel Package

nette/routing

Nette Routing is a standalone two-way router for PHP that maps SEO-friendly URLs to presenter/action pairs and back. It supports route masks, parameters, localization, and is used by the Nette Framework.

View on GitHub
Deep Wiki
Context7

Getting Started

Be honest about the fit first: Laravel's router already owns dispatch, middleware, URL generation and signed URLs. nette/routing is not a Laravel package — it ships no service provider, no facade, no middleware, and no Illuminate contracts. You pull it in when you need a second, independent two-way URL engine alongside Laravel's:

  • a legacy URL scheme you must parse back into parameters (redirect maps, SEO migrations),
  • URL generation shared by a web app, a queue worker and a CLI sitemap generator,
  • strangler-pattern coexistence with an existing Nette app,
  • small standalone PHP tools where booting Laravel is overkill.

Install:

composer require nette/routing   # v3.1.x, PHP 8.1+ (v3.1.0 changelog)

Look first at Nette\Routing\Router — the whole API is two methods: match(IRequest $httpRequest): ?array and constructUrl(array $params, UrlScript $refUrl): ?string. Then RouteList (definition + composition) and Route (mask parsing, encoding).

First use case — a legacy redirect resolver in a Laravel app:

use Nette\Http\Request as NetteRequest;
use Nette\Http\UrlScript;
use Nette\Routing\RouteList;

$legacy = new RouteList;
$legacy->addRoute('', ['target' => 'home']);
$legacy->addRoute('eshop/<category>/<product>', ['target' => 'legacy.product']);
$legacy->addRoute('eshop/<category>', ['target' => 'legacy.category']);

// inbound: parse the URL into params
$request = new NetteRequest(new UrlScript($laravelRequest->fullUrl()));
$params  = $legacy->match($request);       // ['target' => 'legacy.product', 'category' => 'knihy', 'product' => 'php'] or null

// outbound: build a legacy URL from params
$url = $legacy->constructUrl(
    ['target' => 'legacy.product', 'category' => 'knihy', 'product' => 'php'],
    new UrlScript($laravelRequest->fullUrl()),
);

That pair — one definition, both directions — is the entire value proposition. Everything else is glue.

Implementation Patterns

Wrap it in one adapter service. Do not let Nette\Routing types leak into controllers. A single LegacyUrlResolver (constructor-injected RouteList, plus the built-in Nette\Http\RequestFactory or a hand-built Nette\Http\Request) that exposes resolve(string $url): ?array and generate(array $params): string is the right boundary. Swap it for a fake in tests without touching Laravel's container.

Route to the Laravel request only if you must. Nette\Http\Request::fromGlobals() works inside a real HTTP request; for tests, jobs and sub-requests build it explicitly. Routing only reads the URL and method, so a minimal construction is enough:

new NetteRequest(
    new UrlScript($request->fullUrl()),
    $request->query->all(),
    $request->request->all(),
    [], [], [],
    $request->getMethod(),
);

Define routes in config, not code. Return a fully built RouteList from config/legacy_routes.php and bind it as a singleton so the regex compilation happens once per process:

$this->app->singleton(RouteList::class, fn () => require config_path('legacy_routes.php'));

Scope with withPath() / withDomain() / addList(). Both return clones, so capture the result:

$api = $router->withPath('api/v1');   // trailing slash optional since 3.0.5
$api->addRoute('<resource>/<id \d+>', ['target' => 'api.show']);
$router->addList($api);

This is how you model multi-tenant subdomains or versioned APIs without duplicating masks.

Strangler pattern. A tiny middleware tries the legacy router first; on a match, redirect (301) or dispatch to a legacy bridge controller; on null, $next($request). Keep it after Laravel's own routes have had a chance where they overlap, and never let it own 404s — Laravel's exception handler should.

Use one-way routes deliberately. A route that only generates (sitemap entries) or only matches (inbound redirects) prevents an outbound constructUrl() from accidentally picking an inbound-only pattern. Check the Route flag constants for the one-way flag in your version.

Subclass for tenant logic. Since 3.0.5 RouteList::match() is split into prepareRequest() and completeParameters() — override those rather than match() to inject tenant/domain state between the two phases. Subclassing Route gives you a hook to normalise parameters before encoding.

Test it as plain PHP. No HTTP kernel, no database:

$router = require config_path('legacy_routes.php');
$this->assertSame(
    ['target' => 'legacy.product', 'category' => 'knihy', 'product' => 'php'],
    $router->match(new NetteRequest(new UrlScript('https://shop.test/eshop/knihy/php'))),
);

These run in milliseconds and are the best documentation of your URL contract you'll ever have.

Gotchas and Tips

  • constructUrl() returns ?string. It is null — not an exception — when no route can satisfy the parameter set (missing mask parameter, failed constraint). Every call site must null-guard; throw your own exception containing the params so the failure is debuggable.
  • Never pre-encode. urlencode()/rawurlencode() before constructUrl() produces double encoding. Since v3.1.1 Route::param2path() covers all RFC 3986 characters (@ ! ~ etc.) — pin ^3.1.1 if your slugs contain them. param2path() is public, so reuse it for your own slug encoding instead of duplicating rules.
  • UrlScript, not Url. The signature type-hints UrlScript because generation needs the script/base path. new Url($url) will fail; and if you deploy under a subdirectory (php artisan serve aliases, XAMPP), the script path changes how / is generated. Derive it from config, never hardcode https://prod.example.com — queue workers will leak the wrong host into sitemaps.
  • <param> is greedy: [^/]+, dots and spaces included. file/<name> matches file/report.php. Constrain everything user-facing: <id \d+>, <slug [\p{L}\p{N}_-]+>. It won't match empty or slashes, so optional segments need bracket syntax: product/<id>[/<tab>].
  • Order is insertion order. posts/new must precede posts/<id>, or you'll silently get a "post" named new. Walk RouteList::getRoutes() and log each mask to verify ordering; to bisect a mismatch, call Route::match($request) on individual routes instead of the list.
  • withPath() / withDomain() return clones. $router->withPath('admin')->addRoute(...) works, but $router->withPath('admin'); $router->addRoute(...) silently registers at the root.
  • Trailing slashes. withPath() treats a trailing slash as optional (3.0.5), but masks themselves should be written consistently. If Nginx or a Laravel middleware also normalises slashes you can end up in a 301 loop — pick one owner.
  • It matches the path only. Query parameters aren't part of matching; extra keys in the params array become the query string on generation. Don't expect Laravel-style query-string routing.
  • Defaults live in the metadata array (addRoute($mask, ['target' => 'x', 'page' => 1])), and any extra keys become available parameters. Keep them scalar/stringable — objects and closures as defaults were fixed only for SimpleRouter in 3.0.3 and still stringify badly in URL building.
  • Don't dd() a RouteList. Compiled regexes and deep object graphs make dumps unreadable and huge. Map getRoutes() through a small toArray() using getPath(), getDomain(), getFlags() and the mask.
  • Version pins matter. v3.x needs PHP 8.1+; 3.1.2 adds PHP 8.5 support; 3.0.4 relaxed nette/utils/nette/http to 4.0. nette/http is a hard dependency — expect it in your lockfile even though you only wanted routing.
  • SimpleRouter is a useful oddity: one fixed parameter set, matched against every request. Handy as a catch-all fallback or a "maintenance mode" URL generator, and its 3.0.3 fix made closure defaults survivable.
  • Laravel can already do reverse matching. Route::getRoutes()->match(Request::create($url)) parses a URL back into a Laravel route. If your need is only inbound parsing of Laravel-registered routes, you don't need this package at all — reach for nette/routing when you need a URL space Laravel doesn't own.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels