noxlogic/multiparam-bundle
A Symfony bundle that extends routing to support multiple parameters within a single route segment, enabling patterns like /{year}-{month}-{day}. Ideal for cleaner date-based URLs, composite identifiers, and flexible slug structures without custom route logic.
Read this first: noxlogic/multiparam-bundle (repo: jaytaph/MultiParamBundle) is a Symfony 2.x bundle, not a Laravel package. There is no service provider, no facade, no config file that Laravel's PackageManifest will discover. composer require noxlogic/multiparam-bundle in a Laravel app will install classes that Laravel's container and router will never touch. Treat this repository as reference material for a routing pattern, not as a dependency.
The pattern it implements: resolving several placeholders/parameters from the request into a single controller argument before the action runs, driven by annotations on the controller method. In Laravel, that exact behaviour already exists in three native places — route model binding, scoped bindings, and the middleware pipeline — so the day-to-day leverage is "reimplement the idea in ~40 lines" rather than "install it."
Where to look in the repo, in order:
| Path | Why |
|---|---|
README.md |
The only usage documentation; expect Symfony YAML/annotation examples |
Annotation/ |
The actual annotation classes — this is the DSL you'll mirror with PHP 8 attributes |
EventListener/ (or Controller/) |
The kernel event listener that reads annotations and rewrites controller arguments — this is the ~200 lines worth porting |
DependencyInjection/ |
Compiler passes; you can skip these entirely, they only matter for the Symfony container |
First use case to build in Laravel: a composite date route. Given /archive/{year}/{month}/{day}, you want the controller to receive one CarbonImmutable instead of three scalars.
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::bind('day', function (string $value, \Illuminate\Routing\Route $route) {
return \Carbon\CarbonImmutable::createFromDate(
(int) $route->parameter('year'),
(int) $route->parameter('month'),
(int) $value,
)->startOfDay();
});
Route::get('/archive/{year}/{month}/{day}', ArchiveController::class);
// app/Http/Controllers/ArchiveController.php
public function __invoke(int $year, int $month, \Carbon\CarbonImmutable $day)
{
return view('archive', compact('year', 'month', 'day'));
}
That is the entire value proposition of the bundle, expressed in idiomatic Laravel. Confirm it works with php artisan route:list and a feature test before going further.
Pattern 1 — Binding callback with sibling-parameter access. The binding closure receives ($value, $route), which is what makes this a multi-param resolver rather than a single-param one. Use it whenever one URL segment's meaning depends on another (locale + slug, owner + repo, tenant + id).
Route::bind('slug', fn ($slug, $route) => Page::where('locale', $route->parameter('locale'))
->where('slug', $slug)
->firstOrFail());
Pattern 2 — Scoped bindings. If the relationship is already expressed on the model, ->scopeBindings() is the native equivalent of the bundle's "resolve N parameters into one graph" behaviour, and it gives you proper 404s for free.
Route::get('/users/{user}/posts/{post}', [PostController::class, 'show'])->scopeBindings();
// app/Models/Post.php
public function resolveRouteBindingQuery($query, $value, $field = null)
{
return $query->whereBelongsTo($this->user ?? User::findOrFail(request()->route('user')));
}
Pattern 3 — Attribute-driven argument injection (the faithful port). If you genuinely want the bundle's annotation-on-the-method ergonomics, mirror it with PHP 8 attributes and a middleware that runs after SubstituteBindings.
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class MultiParam
{
public function __construct(
public string $argument, // controller parameter name to fill
public array $from, // route parameter names to read
public string $resolver, // invokable class
) {}
}
class ResolveMultiParams
{
public function handle(Request $request, Closure $next)
{
$route = $request->route();
if (! $route || ! is_string($route->getAction('uses') ?? null)) {
return $next($request);
}
[$class, $method] = explode('@', $route->getAction('uses'));
$reflection = new ReflectionMethod($class, $method);
foreach ($reflection->getAttributes(MultiParam::class) as $attribute) {
$meta = $attribute->newInstance();
$inputs = array_map($route->parameter(...), $meta->from);
$route->setParameter($meta->argument, app($meta->resolver)($inputs, $request));
}
return $next($request);
}
}
Register it in bootstrap/app.php (Laravel 11+) or Kernel::$middleware after SubstituteBindings, and remember the controller parameter must match $meta->argument by name to be picked up.
Pattern 4 — Validate the composite, not the parts. The bundle lets annotations declare constraints on the assembled value; do the same with a FormRequest so failures are 422s rather than exceptions from deep inside a resolver.
public function rules(): array
{
return [
'year' => ['required', 'integer', 'between:1990,2100'],
'month' => ['required', 'integer', 'between:1,12'],
'day' => ['required', 'integer', 'between:1,31'],
];
}
Integration tips. Register every Route::bind inside RouteServiceProvider::boot() so bindings exist before any route is matched; keep resolvers as invokable classes with no container lookups so they are trivially unit-testable; memoise reflection results in the attribute middleware (private static array $cache = []) since it runs on every request to an annotated action.
sensio/framework-extra-bundle and doctrine/annotations assumptions, and zero dependents on Packagist. Do not add it to composer.json; vendor the logic under MIT instead and note the origin in a comment.doctrine/annotations 2.x no longer reads them by default). If you port anything, port it to #[Attribute] classes — otherwise you get silent no-ops where the reader returns nothing instead of throwing.SubstituteBindings skips any parameter value that is instanceof UrlRoutable, so a Route::bind('day', ...) closure will never run if some earlier middleware already put a Carbon-like object in that slot. Keep custom binds and implicit model binding on disjoint parameter names.null, not absent. $route->parameter('year') returns null for an unmatched optional segment, and CarbonImmutable::createFromDate(null, ...) will happily give you today's date. Guard explicitly with abort(404) in the resolver.RouteDependencyResolverTrait, a class type-hint that isn't already present in the route parameters is built from the container — so show(Booking $booking) with a booking route key that isn't a real model will hand you an empty Booking instead of a 404. Use implicit binding (matching type-hint + parameter name) or scopeBindings(), not manual setParameter.boot() after route caching is generated won't apply until you re-run php artisan route:cache. Always clear the cache when adding a Route::bind.php artisan route:list --json shows parameter names but not bindings; use Route::getRoutes()->match(Request::create($uri)) in tinker and dump $route->parameters() to see exactly what resolved. If a composite resolves to the wrong value, dd() inside the binding closure — it runs before the controller, so a missing dump means the closure never fired (see the UrlRoutable gotcha).Model::resolveRouteBinding / resolveRouteBindingQuery for per-model composite keys, Router::bind for ad-hoc closure resolution, and ControllerDispatcher (override in the container) if you need true argument injection rather than parameter stuffing. Those three cover everything the bundle does.kernel.controller) has no Laravel analogue.How can I help you explore Laravel packages today?