Ковариантность и Контравариантность
В 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