A Laravel news-archive route injects a service beside {year} and {month?}, and the handler gets swapped values or crashes when month is absent; why, and how do you fix it?
answer
- plain values go by position
- argument names are ignored
- class-typed arguments spliced from the container
- absent optional segment is dropped
- services first, optional last with default
basics
~20 sLaravel passes route values to the handler by position and splices container-resolved services in; an absent optional segment is dropped, so a missing default crashes. List services first, then parameters in URI order, optional ones last with defaults.
solid answer
~40 sLaravel builds the argument list from the route's parameters in URI order, drops absent optional ones, then walks the handler's signature: class-typed arguments such as `Request` or an `ArchiveService` are resolved from the container and spliced in at their positions, and a missing value gets the argument's default if it has one. The array is then spread **positionally**, so `function (string $slug, string $year)` on `/news/{year}/{slug}` receives the year in `$slug`. An optional `{month?}` without a default throws `ArgumentCountError` when absent, and PHP itself drops the default of an optional argument followed by a required one. The fix the docs recommend: services first, then route parameters in URI order, optional ones last with defaults, such as `function (ArchiveService $archive, string $year, ?string $month = null)`.
code
php · 17 lines<?php
namespace App\Http\Controllers;
use App\Services\ArchiveService;
use Illuminate\View\View;
class ArchiveController extends Controller
{
// Route: /news/{year}/{month?}
public function index(ArchiveService $archive, string $year, ?string $month = null): View
{
return view('news.archive', [
'articles' => $archive->list($year, $month),
]);
}
}go deeper
Keep handler arguments in the same order as the URI placeholders and give optional segments a default.
Explain that values are passed by position, how class-typed arguments are resolved and spliced in, and why an absent optional segment needs a default.
Diagnose swapped values and ArgumentCountError from the signature alone, including PHP's dropped default before a required argument, and enforce services-first ordering in reviews.
Adopt a signature convention for actions across the codebase so injection and route parameters never compete, and back it with static analysis where possible.
## How Laravel builds the argument list When a route matches, the framework dispatches to a closure (`CallableDispatcher`) or a controller method (`ControllerDispatcher`). Both use the same resolver, the `ResolvesRouteDependencies` trait: 1. Start from the route's parameters **in URI order** (captured domain parameters, if any, come first), with absent (null) optional parameters removed. 2. Walk the handler's reflected arguments one by one. 3. For an argument whose type is a class not already present among the values, resolve it from the **service container** and splice it into the list at that argument's position. 4. For a plain argument with no value left at its position, splice in its **default value**, if PHP reports one. 5. Call the handler with `...array_values($list)`: the keys are thrown away and the values are passed **positionally**. Step 5 is the heart of the matter: parameter **names** play no part for plain values. Route model binding, which looks at names to decide what to load, is a separate mechanism that runs before this step. ## Bug 1: swapped values ```php Route::get('/news/{year}/{slug}', function (string $slug, string $year) { // /news/2026/budget-vote -> $slug = '2026', $year = 'budget-vote' }); ``` No error is raised: both arguments are strings, so the wrong values flow into queries and templates. The fix is to keep argument order identical to URI order. ## Bug 2: an absent optional segment without a default ```php Route::get('/news/{year}/{month?}', function (string $year, string $month) { // /news/2026 -> ArgumentCountError: too few arguments }); ``` The month parameter is absent, so only one value exists, and `$month` has no default for the resolver to splice in. ## Bug 3: a default PHP silently discards ```php function (string $year, ?string $month = null, ArchiveService $archive) ``` This looks safe, but PHP compiles an argument with a default that is **followed by a required argument** as required and drops the default (usually with a deprecation notice). For `/news/2026`, the resolver finds no default for `$month`, splices the service at position three, and the call receives two values for three required arguments: another `ArgumentCountError`. ## The fix | Rule | Why | |---|---| | Container-resolved services first | their positions never depend on which segments are present | | Route parameters next, in URI order | positional passing then lines them up | | Optional segments last, each with a default | an absent value falls back cleanly, and PHP keeps the default | ```php Route::get('/news/{year}/{month?}', function ( ArchiveService $archive, string $year, ?string $month = null, ) { return $archive->list($year, $month); }); ``` This is the ordering the routing docs recommend ("list your route parameters after your dependencies"). The resolver can splice services into other positions in simple cases, but relying on that makes every optional segment a potential bug. ## A review checklist for action signatures When reviewing a route closure or controller action that takes route parameters: 1. Are all class-typed arguments (the request, services) at the front? 2. Do the remaining arguments appear in the same order as the placeholders in the URI, including any from a route prefix? 3. Does every optional placeholder have a matching argument with a default, and is it at the end? 4. Is any argument with a default followed by a required one? If so, PHP has already dropped that default. 5. Do the argument names match the placeholders anyway? They are not used for plain values, but matching names keep the code readable and are required once route model binding enters the picture. ## How to diagnose it quickly - **Wrong values, no error**: compare argument order with URI order; names do not matter. - **`ArgumentCountError` only on the short URL**: an optional segment lacks a usable default, or a required argument follows it. - **A service where a string should be**: the argument list shifted; move services to the front. - In controllers the same rules apply to action methods; constructor injection is unaffected because constructors are resolved separately. ## Pitfalls interviewers probe - Believing Laravel matches plain route values to arguments **by name**. - Believing `?string $month = null` is always optional, even before a required argument. - Fixing a swap by renaming placeholders instead of reordering arguments. - Blaming the container when the real cause is a missing default.
- Why does ?string $month = null placed before a required ArchiveService argument still crash on /news/2026?PHP compiles an argument that has a default but is followed by a required argument as required, and discards the default. Reflection then reports no default, so Laravel's resolver has nothing to splice in for the absent month. The service is spliced at its position, the call receives too few values, and PHP throws `ArgumentCountError`.
- Does the same ordering rule apply to a controller's constructor dependencies?No. Constructor arguments are resolved by the container when the controller is built and never receive route values. The positional rules apply only to the action method (or the route closure), which is where route parameters and method-injected services share one argument list.
saying these in an interview costs you the question
- Laravel passes plain route parameters to arguments with matching names
- Swapped argument names always raise an error
- ?string $month = null stays optional even before a required argument
- Services must always be the first arguments or injection fails
- An absent optional segment is passed as null even without a default