Why is renaming a function or method parameter a breaking change since PHP 8.0, and how do you keep parameter names stable in a library?
answer
- names became part of the call
- resolved against the method that runs
- no inheritance check on names
- a variadic swallows the old name
- PHPStan 2.2: parameterRenamedInSubtype
basics
~20 sSince PHP 8.0 callers can pass arguments by name, so a parameter name is public API: renaming it makes existing named calls throw Error: Unknown named parameter. PHP never checks names across inheritance, so authors must keep them stable.
solid answer
~50 sNamed arguments bind by **parameter name**, so once PHP 8.0 shipped them every name in a public signature became API. Rename `$rows` to `$data` and a caller's `export(rows: $r)` throws `Error: Unknown named parameter $rows`, even though positional callers see no change. Names resolve against the method that **actually runs**, and PHP's signature compatibility check ignores names, so an implementation may legally rename an interface's parameter; the manual only recommends keeping them the same. A call written against the interface then breaks for that one implementation. If the implementation has a variadic, the old name is silently collected instead. So in a library: keep names identical across interfaces and overrides, treat a rename as a breaking change in your versioning, choose names carefully up front, and let PHPStan 2.2 flag named calls to parameters that an override renames.
code
php · 24 lines<?php
declare(strict_types=1);
interface ReportExporter
{
public function export(array $rows, string $format = 'csv'): string;
}
final class FileExporter implements ReportExporter
{
// Legal: PHP does not compare parameter names with the interface
public function export(array $data, string $format = 'csv'): string
{
return $format . ':' . count($data);
}
}
function run(ReportExporter $exporter): string
{
return $exporter->export(rows: [1, 2, 3], format: 'json');
}
echo run(new FileExporter());
// Fatal error: Uncaught Error: Unknown named parameter $rowsgo deeper
Recall that since PHP 8.0 callers can use parameter names, so renaming a public parameter can break their code.
Explain why the error appears only at runtime and only for the class that renamed the parameter, and why positional callers never notice.
Show how you would audit a hierarchy for renamed parameters, handle a needed rename in a release, and why variadic option bags make it worse.
Decide and publish a compatibility policy for parameter names across a library's public surface, balancing naming freedom against callers' use of named arguments.
## Why names became API Before PHP 8.0 a caller could only pass arguments by **position**. The name of a parameter was a local variable name, visible only inside the function, and a library could rename it freely. PHP 8.0 added **named arguments** (`export(rows: $r, format: 'json')`), which bind a value to a parameter **by its name**. From that release on, every parameter name of a public function or method is something callers may type, and so something they depend on. The resulting compatibility picture differs between the two call styles: | Signature change | Positional callers | Named-argument callers | |---|---|---| | Rename a parameter | unaffected | `Error: Unknown named parameter $old` | | Reorder two parameters | values land in the wrong slots | unaffected | | Append an optional parameter | unaffected | unaffected | | Remove an optional parameter | extra argument silently ignored by a user function | `Error: Unknown named parameter` | Renaming is the change that looks safe to someone thinking only positionally, which is why it is the one that bites. ## Renames inside a class hierarchy A named argument is resolved **at runtime against the function that actually executes**. For a method call, that is the implementation on the object's class, not the interface or parent the caller typed against. PHP's inheritance rules check parameter count, types and by-reference markers for compatibility, but **not names**, so the following compiles without a notice: - the interface declares `export(array $rows, string $format = 'csv')`; - an implementation declares `export(array $data, string $format = 'csv')`. A caller holding a `ReportExporter` and writing `$exporter->export(rows: $rows)` is correct against the interface. It works for every implementation that kept `$rows` and throws `Error: Unknown named parameter $rows` for the one that renamed it. The failure shows up only at runtime and only for that class. The manual's advice on interfaces is exactly this: implementations may use different names, but callers may rely on the interface's names, so keep them the same. ## A variadic turns the error into silence If the implementation also declares a variadic parameter such as `...$options`, an unknown named argument is **not** an error: the engine collects it into that array under a string key (`['rows' => [...]]`). If the renamed parameter has a default, the call then succeeds with the default instead of the caller's value. A loud `Error` becomes a silent wrong result, which is worse. ## Keeping names stable in a library 1. **Treat every public parameter name as API.** Pick the name with the same care as the method name; renaming it later is a breaking change. 2. **Mirror the names of interfaces and parent methods exactly** in every implementation and override, including in your own internal classes. 3. **Version renames as breaks.** Under semantic versioning, a rename belongs in a major release with a changelog line, like any other signature break. Some libraries instead publish a policy that named-argument use is not covered by their compatibility promise; either way, say so explicitly. 4. **Let static analysis watch the hierarchy.** PHPStan 2.2 reports, from level 0, `Call to Foo::doFoo() uses named argument for parameter $order, but FooImpl renames it to $number.` (identifier `argument.parameterRenamedInSubtype`). It needs to see both the named call and the renaming override in the analysed code. A library cannot run it over its consumers' calls, so it catches renames inside one codebase, not across a published boundary. 5. **Avoid variadic option bags on methods whose names matter**, or validate the collected keys, so a stale name fails instead of disappearing. ## What does not help - There is **no alias syntax**: a parameter has exactly one name, so you cannot accept both `$rows` and `$data` in one signature. - A deprecation period for the old name needs a hand-written shim, such as a variadic that detects the old key, maps it and triggers a deprecation, and that shim carries the silent-collection risk above. - Telling consumers to "just use positional arguments" does not remove the risk: you cannot see how they call you, and PHP gives them no warning until the renamed version is installed and the named call runs. ## A review checklist When a pull request touches a public signature, the questions to ask are short: - Did any parameter name change, in the method itself or in an interface it implements or a parent it overrides? - Does any override in the codebase use a name that differs from its prototype? - Does the change add a variadic that could start collecting names that used to be errors? - Is the release that carries a rename marked as breaking?
- What changes if FileExporter::export() also declares a variadic ...$options parameter and gives $data a default?The call no longer throws. The engine collects the unmatched `rows:` argument into `$options` as `['rows' => [1, 2, 3]]`, and `$data` takes its default. The method returns a result computed without the caller's rows, so a clear runtime Error has become silent wrong data.
- Does PHP warn when a subclass or interface implementation renames a parameter?No. The signature compatibility check looks at arity, types and by-reference markers, not at names; the manual only recommends keeping the interface's names. The mismatch surfaces only when a named call reaches that class. PHPStan 2.2 reports it at level 0 as `argument.parameterRenamedInSubtype`, provided it sees both the named call and the override.
saying these in an interview costs you the question
- Renaming a parameter is safe as long as its type and position stay the same.
- PHP refuses to compile a class whose method renames an interface's parameter.
- A named argument resolves against the interface the caller typed against.
- Adding a variadic ...$options makes old parameter names keep working correctly.
- Running PHPStan on a library protects consumers' named calls from its renames.