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

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

Объявление типа считается более конкретным в следующем случае:

  • Тип удаляется из типа объединения
  • Тип добавляется к типу пересечения
  • Тип класса изменен на тип дочернего класса
  • iterable заменяется на массив или Traversable

Класс типа считается менее конкретным, если верно обратное.

Ковариация

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

class Cat extends Animal 
{
    public function speak()
    {
        echo $this->name . " meows";
    }
}

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

<?php

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

class CatShelter implements AnimalShelter
{
    public function adopt(string $name): Cat // instead of returning class type Animal, it can return class type Cat
    {
        return new Cat($name);
    }
}

class DogShelter implements AnimalShelter
{
    public function adopt(string $name): Dog // instead of returning class type Animal, it can return class type Dog
    {
        return new Dog($name);
    }
}

$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();

Приведенный выше пример выведет:

Рики мяукает
Маврик лает

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

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

<?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 . " eats " . get_class($food);
    }
}

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

<?php

class Dog extends Animal
{
    public function eat(Food $food) {
        echo $this->name . " eats " . get_class($food);
    }
}

Следующий пример покажет поведение контравариантности.

<?php

$kitty = (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo "\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);

Приведенный выше пример выведет:

Рики ест AnimalFood
Маврик ест еду

Но что произойдет, если $kitty попытается съесть $ banan ?

$kitty->eat($banana);

Приведенный выше пример выведет:

Неустранимая ошибка: Uncaught TypeError: аргумент 1, переданный в Animal::eat(), должен быть экземпляром AnimalFood, переданным экземпляром Food