Ковариантность и Контравариантность

До версии 7.4.0, в PHP, типы параметров и возвращаемых значений, по большей части были инвариантны. Это значит, что если метод родительского класса принимал параметром или возвращал значение типа T, то и в дочернем классе соответствующий параметр или возвращаемое значение обязаны быть того же типа T.

Начиная с PHP 7.4.0 появилась поддержка ковариантности и контравариантности. Звучит страшно, но на самом деле всё просто и очень полезно в объектно ориентированном подходе.

Ковариантность

Ковариантность позволяет дочернему методу возвращать более конкретный подтип типа, возвращаемого родительским методом. Проще всего показать на примере.

Возьмём простой абстрактный родительский класс Animal и создадим на его основе два дочерних класса Cat и Dog.

<?php

abstract class Animal
{
    protected 
string $name;

    public function 
__construct(string $name)
    {
        
$this->name $name;
    }

    abstract public function 
speak();
}

class 
Dog extends Animal
{
    public function 
speak()
    {
        echo 
$this->name " лает";
    }
}

class 
Cat extends Animal 
{
    public function 
speak()
    {
        echo 
$this->name " мяукает";
    }
}

Обратите внимание, что в примере отсутствуют методы возвращающие какие либо значения. В следующем примере мы создадим несколько фабрик, возвращающих объекты типов Animal, Cat и Dog. Именно на этом примере мы продемонстрируем ковариантность.

<?php

interface AnimalShelter
{
    public function 
adopt(string $name): Animal;
}

class 
CatShelter implements AnimalShelter
{
    public function 
adopt(string $name): Cat // Возвращаем класс Cat вместо  Animal
    
{
        return new 
Cat($name);
    }
}

class 
DogShelter implements AnimalShelter
{
    public function 
adopt(string $name): Dog // Возвращаем класс Dog вместо  Animal
    
{
        return new 
Dog($name);
    }
}

$kitty = (new CatShelter)->adopt("Рыжик");
$kitty->speak();
echo 
"\n";

$doggy = (new DogShelter)->adopt("Бобик");
$doggy->speak();

Результат выполнения данного примера:

Рыжик мяукает
Бобик лает

Контравариантность

С другой стороны, контравариантность позволяет дочернему методу принимать параметром менее конкретный тип, чем задано в родительском. В продолжение предыдущего примера, где мы использовали классы Animal, Cat и Dog, мы введем новые классы Food и AnimalFood и добавим в абстрактный класс Animal новый метод eat(AnimalFood $food).

<?php

class Food {}

class 
AnimalFood extends Food {}

abstract class 
Animal
{
    protected 
string $name;

    public function 
__construct(string $name)
    {
        
$this->name $name;
    }

    public function 
eat(AnimalFood $food)
    {
        echo 
$this->name " хомячит " get_class($food);
    }
}

Чтобы увидеть суть контравариантности, мы переопределим метод eat класса Dog таким образом, чтобы он мог принимать любой объект класса Food. Класс Cat оставим без изменений.

<?php

class Dog extends Animal
{
    public function 
eat(Food $food) {
        echo 
$this->name " хомячит " get_class($food);
    }
}

Теперь мы можем увидеть, как работает контравариантность.

<?php

$kitty 
= (new CatShelter)->adopt("Рыжик");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo 
"\n";

$doggy = (new DogShelter)->adopt("Бобик");
$banana = new Food();
$doggy->eat($banana);

Результат выполнения данного примера:

Рыжик хомячит AnimalFood
Бобик хомячит Food

Но что случится, если $kitty попробует съесть (eat) банан ($banana)?

$kitty->eat($banana);

Результат выполнения данного примера:

Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given