Inheritance

Method Compatibility

When a class extends another class (or implements an interface) then for each method that is overwritten (or implemented) the compatibility has to be checked.

A method declared in a child class that overrides a method of the same name that is already present in the parent class may be contravariant and accept a more general parameter as well as covariant and return a more specific type. This means that the type hierarchy shown below is valid:

class A
{
}

class B extends A
{
}

class X
{
    public function m(): A
    {
        return new A;
    }
}

class Y extends X
{
    public function m(): B
    {
        return new B;
    }
}

Parameter type contravariance and return type covariance are only fully supported when all classes and interfaces involved are available before they are referenced. This means that edge cases are possible where otherwise valid type hierarchies are rejected by PHP’s compiler when preloaded is used instead of autoloading.

Prior to PHP 7.4, the implementation of this method compatibility check was too restrictive and did not allow to remove parameter type declarations when overwriting an inherited method, for instance:

class ParentClass
{
    function m(int $x)
    {
    }
}

class ChildClass extends ParentClass {
    function m($x)
    {
    }
}

Executing the code shown above with earlier versions than PHP 7.4 would have lead to the warning shown below:

Warning: Declaration of ChildClass::m($x) should be compatible with
ParentClass::m(int $x) in ...

Now the code shown above does not trigger that warning anymore. This is in line with the Liskov Substitution Principle – the L in SOLID – which allows for contravariance of method parameter types. The removal of a type declaration from a method parameter is equivalent to replacing the method parameter’s original type with the mixed type. This type, which we do not (explicitly) have in PHP, allows for argument values of any type for the parameter in question.

Prior to PHP 7.4 there was a bug that caused the method compatibility check to be performed against the declaration of a method in the farthest-away ancestor of the inheritance chain.

Since PHP 7.4 the methods of a child class are checked against the methods in the nearest ancestor who (re)declares the method.

Below you will find a couple of examples that showcase different manifestations of this problem:

interface I
{
    public function m($a, $b, $c);
}

class A implements I
{
    public function m($a, $b = null, $c = null)
    {
    }
}

class B extends A
{
    public function m($a, $b, $c = null)
    {
    }
}

Executing the code shown above with earlier versions of PHP triggers the error shown below:

Fatal error: Declaration of B::m($a, $b, $c = NULL) must be
compatible with A::m($a, $b = NULL, $c = NULL) in ...
interface I
{
    public function m();
}

class A implements I
{
    public function m(): int
    {
    }
}

class B extends A
{
    public function m(): string
    {
    }
}

Now the code shown above triggers the error shown below:

Fatal error: Declaration of B::m(): string must be
compatible with A::m(): int in ...
interface I
{
    public function m();
}

class A implements I
{
    public function m(): int
    {
    }
}

class B extends A
{
    public function m(): string
    {
    }
}

The code shown above triggers the error shown below:

Fatal error: Declaration of B::m(): string must be
compatible with A::m(): int in ...
abstract class A
{
    abstract function m($a, $b, $c);
}

class B extends A
{
    function m($a, $b = null, $c = null)
    {
    }
}

class C extends B
{
    function m($a, $b, $c = null)
    {
    }
}

The code shown above triggers the error shown below:

Fatal error: Declaration of C::m($a, $b, $c = NULL) must be
compatible with B::m($a, $b = NULL, $c = NULL) in ...
abstract class A
{
    abstract function example();
}

class B extends A
{
    function example(): int
    {
    }
}

class C extends B
{
    function example(): string
    {
    }
}

The code shown above triggers the error shown below:

Fatal error: Declaration of C::example(): string must be
compatible with B::example(): int in ...

The errors shown above were not triggered by earlier versions of PHP. While these errors disallow class declarations that were possible before, these restrictions cannot really be considered a break of backward compatibility as the declarations allowed by earlier versions of PHP were confusing at best and incorrect at worst.

The Liskov Substitution Principle ensures that a child class can be used as a substitute for its parent. The following code should therefore be considered invalid:

class ParentClass
{
    public function method($a)
    {
    }
}

class ChildClass extends ParentClass
{
    public function method($a, $b)
    {
    }
}

Nevertheless, PHP used to treat violations of this rule with a mere E_STRICT notice. To better reflect the severity of this mistake, PHP 7 now emits an E_WARNING. Given that E_STRICT notices could safely be ignored, this change may break backwards compatibility.

Constructor Access

When a method is overwritten in a child class then its signature must match that of the original method. For instance, the number of required arguments as well as the parameter types must be the same:

class ParentClass
{
    public function method(string $a, int $b)
    {
    }
}

class ChildClass extends ParentClass
{
    public function method(int $a)
    {
    }
}

Executing the code shown above triggers the warning shown below:

Warning: Declaration of ChildClass::method(int $a) should be compatible
with ParentClass::method(string $a, int $b) in ...

The constructor, however, is exempt from these rules:

class ParentClass
{
    public function __construct(string $a, int $b)
    {
    }
}

class ChildClass extends ParentClass
{
    public function __construct(int $a)
    {
    }
}

The code shown above does not trigger a warning.

While PHP always treated the constructor differently than regular methods, it did so inconsistently with regard to visibility:

class ParentClass
{
    public function __construct()
    {
    }
}

class ChildClass extends ParentClass
{
    protected function __construct()
    {
    }
}

Previously, executing the code shown above triggered the compile-time error shown below:

Fatal error: Access level to ChildClass::__construct() must be
public (as in class ParentClass) in ...

Now PHP allows a child class to restrict access to the constructor and does not trigger the error shown above. This change does not break backward compatibility as it only removes a restriction.

Of course, trying to create an instance of ChildClass will still result in a runtime error because the constructor of that class is not public.