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

В PHP 7.2.0 была добавлена частичная противоположность путем устранения ограничений типа для параметров в дочернем методе. Начиная с PHP 7.4.0, добавлена полная поддержка ковариации и контравариантности.

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

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

Чтобы проиллюстрировать, как работает ковариация, создан простой абстрактный родительский класс Animal. 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