skip to content

In PHP, which changes to a method's signature may an overriding method make, and which ones does the engine reject?

level: middleimportance: should knowfreq 48%

answer

  1. parameters may widen, returns may narrow
  2. required parameter count cannot grow
  3. visibility may only relax
  4. by-reference parameters are invariant
  5. Declaration of ... must be compatible with ...

basics

~20 s

An override may widen parameter types, narrow the return type, add optional parameters and relax visibility. PHP rejects extra required or removed parameters, narrower parameter types, wider or dropped return types, stricter visibility and by-reference or static changes.

solid answer

~50 s

PHP checks every override when the child class is declared. The child may **widen parameter types** (contravariance, including dropping a type), **narrow the return type** (covariance, full support since 7.4), **add optional parameters**, make a required parameter optional, and **relax visibility** from `protected` to `public`. It rejects adding a required parameter, removing a parameter, narrowing a parameter type, widening or removing a declared return type, changing a parameter's by-reference mode and turning a variadic into a fixed list, all with `Declaration of Child::m(...) must be compatible with Parent::m(...)`; only for tentative return types of internal classes is a return mismatch a deprecation instead. Stricter visibility fails with `Access level to ... must be public (as in class ...)`, and switching between static and instance fails too. Constructors are exempt unless the parent's is abstract or comes from an interface, and a parent's private method imposes no rules at all.

code

php · 20 lines
php
<?php
declare(strict_types=1);

class Notification
{
    protected function send(string $to, string $body): bool|string
    {
        return true;
    }
}

class SmsNotification extends Notification
{
    // Legal: public widens visibility, string|int widens $to,
    // $parts is optional, and bool narrows the return type.
    public function send(string|int $to, string $body, int $parts = 1): bool
    {
        return true;
    }
}

go deeper

for a junior

Recall the short version: parameters can accept more, returns can promise more, visibility can only open up, and new parameters must be optional.

for a middle

Explain each rule from the caller holding a parent reference, list the rejected changes with their error messages, and name the constructor and private-method exemptions.

for a senior

Show you can read a 'must be compatible' error quickly and choose a fix that keeps substitutability, such as moving data into the constructor instead of widening a method.

for a principal

Discuss how return-type additions in a base library ripple into every subclass, and how to phase such signature changes across teams that extend your classes.

## When and why PHP checks overrides When a class that `extends` another is declared, the engine compares every overriding method with the parent method it replaces. The rule behind the checks is **substitutability**: code written against the parent must keep working when it receives a child. A mismatch is a fatal compile error at class declaration, reported as: `Declaration of SmsNotification::send(...) must be compatible with Notification::send(...)` so broken hierarchies fail on load, not on the first unlucky call. ## What an override may change - **Widen a parameter type** (contravariance): `string $to` may become `string|int $to`, or lose its type entirely. Partial contravariance arrived in 7.2, full variance in 7.4. - **Narrow the return type** (covariance): `bool|string` may become `bool`, and a parent type may become a child class type. - **Add a return type** where the parent declared none. - **Add optional parameters** after the existing ones, or give an existing required parameter a default. - **Relax visibility**: `protected` may become `public`. - Change default values, or rename parameters (names are not part of the compatibility check). A type is **narrower** when a member is removed from a union, a member is added to an intersection, a class is replaced by a subclass, or `iterable` becomes `array` or `Traversable`; the opposite makes it wider. ## What the engine rejects | Change in the child | Result | |---|---| | extra **required** parameter | `must be compatible` error | | a parameter **removed** | `must be compatible` error | | parameter type **narrowed** | `must be compatible` error | | return type **widened** or **dropped** | `must be compatible` error (a deprecation instead, for tentative return types of internal classes) | | `&$x` vs `$x` mismatch on a parameter | `must be compatible` error (by-reference is invariant) | | parent returns by reference, child does not | `must be compatible` error | | variadic parameter turned into a fixed list | `must be compatible` error | | `public` narrowed to `protected` or `private` | `Access level to ... must be public (as in class ...)` | | instance method made `static`, or the reverse | `Cannot make non static method ... static` (or the reverse) | | concrete method redeclared `abstract` | `Cannot make non abstract method ... abstract` | | method is `final` in the parent | `Cannot override final method ...` | The required-parameter rule follows from PHP's arity model: passing too **few** arguments is an error, so a child needing more would break parent-typed callers. Removing a parameter is rejected for the matching reason. ## The exemptions 1. **Constructors** are not checked for signature or visibility against a concrete parent constructor. A child may take different constructor parameters, or even make the constructor `private`. They are checked only when the parent constructor is `abstract` or declared in an interface. 2. **Private parent methods** impose nothing: the child's same-named method is unrelated, so any signature is legal. 3. **Internal classes** with tentative return types (since 8.1) produce an `E_DEPRECATED` notice rather than a fatal error when the child's return type is missing or incompatible. 4. Since **PHP 8.5**, a `final` subclass may replace a `static` return type with `self` or its own class name, because no further subclass can exist. ## A quick checklist before writing an override - Keep every parameter the parent has, in the same order, with the same `&` marks. - Any new parameter gets a default value. - Each parameter type is the parent's type or something wider. - The return type is the parent's type or something narrower, and it is present if the parent declares one. - Visibility is the parent's or more open. - `static` matches the parent. If all six hold, the engine accepts the override. Whether the override also keeps the parent's **behaviour** (its documented results, exceptions and side effects) is a design question the engine cannot check; the signature rules only guarantee that the call itself still type-checks. ## Reading the error message The error prints both full signatures, child first. Compare them position by position: count required parameters, then each parameter's type and `&`, then the return type. Most real cases are one of three: a child added a required parameter, a child narrowed a parameter from an interface-typed value to one concrete class, or a library update gave a parent method a return type that the child lacks.

  • EmailNotification genuinely needs a subject. How do you add it without breaking the override rules?
    Make it optional, `string $subject = ''`, so parent-typed callers still work, or move it into the constructor so `send()` keeps the parent's signature. If the data only exists for email, the constructor route is usually cleaner, because callers holding a `Notification` never know about subjects.
  • Why may a parameter type widen but not narrow in an override?
    A caller holding the parent type may pass anything the parent accepts. If the child accepts more, every such call still works; if it accepts less, some calls that were valid against the parent would now fail. The return type runs the other way: callers expect the parent's return type, and a narrower child result still satisfies them.
  • Why can a child constructor take completely different parameters from its parent's constructor?
    Constructors are not called through a parent-typed reference: `new` always names the concrete class. PHP therefore skips signature and visibility checks for constructors unless the parent constructor is abstract or comes from an interface, in which case the ordinary rules apply.

saying these in an interview costs you the question

  • A child may narrow a parameter type to a more specific subclass
  • Adding a required parameter to an override is fine if callers pass it
  • A child may reduce a public method to protected to hide it
  • Dropping a declared return type in the child is always allowed
  • Constructors must keep exactly the parent's parameter list