skip to content

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?

level: seniorimportance: should knowfreq 30%

answer

  1. names became part of the call
  2. resolved against the method that runs
  3. no inheritance check on names
  4. a variadic swallows the old name
  5. PHPStan 2.2: parameterRenamedInSubtype

basics

~20 s

Since 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 s

Named 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
<?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 $rows

go deeper

for a junior

Recall that since PHP 8.0 callers can use parameter names, so renaming a public parameter can break their code.

for a middle

Explain why the error appears only at runtime and only for the class that renamed the parameter, and why positional callers never notice.

for a senior

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.

for a principal

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.