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 Laravel Package

mezon/router

Mezon Router is a fast, lightweight routing library for PHP with support for route groups, middleware, controllers, and RESTful routes. It provides a simple API for defining and dispatching routes, making it a flexible choice for small to medium PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install from the front controller and dispatch from one entry point — that's the entire bootstrap:

composer require mezon/router
// public/index.php
require __DIR__ . '/../vendor/autoload.php';

$router = new \Mezon\Router\Router();

$router->addRoute('/', fn() => 'Home');
$router->addRoute('/hello/', fn() => 'Hello world');
$router->addRoute('/user/[i:id]/', function () use ($router) {
    return 'User #' . $router->getParam('id');
});

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
echo $router->callRoute($path);

Where to look first: src/Router.php and src/Route.php, then the tests/ directory. For a package this small the tests are effectively the API reference — the README covers placeholder syntax but not the edge cases (trailing slashes, method disambiguation, optional segments). The tests cover both.

First realistic use case: a small standalone API or a legacy switch/include dispatch that you want to replace incrementally with typed URL segments (/orders/[i:id]/items/[i:item]), without adopting Symfony Routing or the Laravel HTTP kernel. addRoute() + callRoute() is a complete request cycle. As a Laravel developer, treat this as the router you reach for when you are deliberately not in Laravel — a microservice, a health-check endpoint, a webhook receiver, or a CLI-adjacent entry point — not as a replacement for Route::get().

Implementation Patterns

Typed placeholders are the core value proposition. Segments are declared [<type>:<name>], not {name}. Validation happens before the handler runs, so handlers can trust the type instead of re-validating raw input:

$router->addRoute('/post/[s:slug]/',    fn() => renderPost($router->getParam('slug')));
$router->addRoute('/price/[f:amount]/', fn() => formatMoney($router->getParam('amount')));
$router->addRoute('/files/[a:tail]/',   fn() => servePath($router->getParam('tail')));

This is closest in spirit to Laravel's where('id', '[0-9]+'), but inverted: the constraint is part of the segment, and an unmatched /user/abc/ never reaches your closure.

HTTP method is the third argument — register per verb. Do not assume one route handles all methods. Either register the same path twice with different verbs, or dispatch on $_SERVER['REQUEST_METHOD'] yourself:

$router->addRoute('/users/', fn() => listUsers(), 'GET');
$router->addRoute('/users/', fn() => createUser($body), 'POST');

Treat the router as a boundary, not an application. The most maintainable integration keeps handlers to one or two lines that delegate into a service or container — the router has no DI, no model binding, no automatic controller resolution:

$router->addRoute('/users/[i:id]/', function () use ($router, $container) {
    $user = $container->get(UserRepository::class)->find($router->getParam('id'));
    return $container->get(UserPresenter::class)->toJson($user);
}, 'GET');

If you want "controllers," wire them by hand: fn() => $container->get(UserController::class)->show($router->getParam('id')).

Centralize registration in a routes.php that returns a configured Router. The description advertises route groups, but day-to-day you will most likely build prefixes yourself (const API = '/api/v1'; plus concatenation) in one file rather than scattering addRoute() calls. One file also makes it trivial to write a route-inventory test that asserts every route resolves — your best defense against the "route definition drift" failure mode when this sits next to a Laravel app.

Testing without a web server is a genuine selling point. callRoute() returns the handler's return value directly, so unit tests reduce to:

$router = new Router();
$router->addRoute('/ping/', fn() => 'pong');
$this->assertSame('pong', $router->callRoute('/ping/'));

Add negative cases (/user/abc/ does not match [i:id]), trailing-slash cases, and one test per HTTP method — these are exactly the behaviors the package's own tests document, and they are where behavior diverges from what a Laravel developer expects.

404 handling goes through a catch-all or a default route. Register it explicitly and set the HTTP status from your dispatcher — the router does not emit status codes for you. Build a single dispatch() wrapper that converts "no match" into a 404 and a bad method into a 405, so the semantics are consistent across every endpoint.

If you need middleware, put it above the router. Wrap callRoute() in your own pipeline (auth check, CORS, JSON body parse) rather than relying on a package-level middleware concept. This keeps the router swappable and keeps security assumptions visible in one place.

Gotchas and Tips

  • Placeholder syntax is not Laravel's. [i:id] ≠ {id}. Copy-pasting Laravel route definitions silently produces routes that never match. Type codes matter: i int, s string, f float, a "any/tail" — verify the full set in src/Route.php before betting a URL scheme on it.
  • Trailing slashes are significant. /hello/ and /hello are likely different routes. Pick one convention, enforce it in your dispatcher (normalize the path before callRoute()), and test it. This is the single most common source of "works locally, 404s in prod."
  • The description promises route groups and a middleware pipeline; the API surface is thinner than that. Verify both against the tests before designing around them. If groups are absent, a prefix helper in routes.php is a ten-line substitute — do not build abstractions on advertised features you haven't seen in code.
  • The router does not set HTTP status codes, headers, or content types. callRoute() returns whatever the handler returns. Your dispatcher owns response formatting. Return arrays/strings consistently and serialize in one place.
  • Method-override, HEAD/OPTIONS handling, and 405 semantics are yours to implement. There is no Route::match(), no automatic OPTIONS, no HEAD fallback. If an API gateway fronts this, verify preflight behavior explicitly.
  • No named routes, no reverse URL generation. Every url('/users/'.$id) is manual string building. That's fine for a bounded service; it's disqualifying for anything that templates links from route names. Note it early in a design review, because it's the feature people discover last and miss most.
  • The request/response model is the package's own — not PSR-7/PSR-15. Don't expect to drop it into an existing PSR-7 middleware stack. Treat it as the innermost layer of a front controller you control.
  • Zero dependents and 271 stars means you own it. Pin the exact version in composer.json, commit composer.lock, and add a CI job that watches upstream releases. Vendor or fork if it lands on a critical path. The exit path is cheap precisely because the surface is small — but only if you kept the interface thin.
  • Wrap it before you ship it. Even for standalone use, put a Router interface in your own namespace (add(), dispatch(), param()) and implement it with mezon/router. Swapping to Symfony Routing or FastRoute then becomes a single class, and your handlers and tests stay untouched.
  • Do not introduce it into a Laravel app as a second router. Two routing systems means two 404 behaviors, two middleware stories, duplicated route definitions, and an auth-bypass risk on any endpoint the Laravel kernel no longer sees. If you must run it beside Laravel, confine it to a separate front controller / service boundary that shares no authenticated routes.
  • Debugging tip: when a route "doesn't exist," log the exact path string your dispatcher passed to callRoute() (post-parse_url, post-normalization) alongside the registered patterns. Almost every mismatch is a trailing slash, a query string leaking into the path, or a %20-style encoded segment versus the literal.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource