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

Multiparam Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • The bundle is archived and Symfony-only. PHP 5.3-era syntax, 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.
  • Annotations vs. attributes. The bundle reads docblock annotations. Modern PHP 8 deprecates that path entirely (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.
  • Binding callbacks do not fire for already-bound models. 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.
  • Optional parameters arrive as 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.
  • Type-hinted controller arguments are resolved by the container before route parameters. In 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.
  • Ordering matters. Bindings registered in a service provider's 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.
  • Debugging. 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).
  • Extension points you actually want: 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.
  • Verdict on adoption: 16 stars, archived, score 0.31 — use it as a documentation reference for the annotation pattern and nothing more. Any code you take from it should be re-tested against your Laravel version, because the event-listener lifecycle it was written for (kernel.controller) has no Laravel analogue.
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