Covariância e Contravariância

No 7.2.0, a contravariância parcial foi introduzida removendo as restrições de tipo nos parâmetros de um método filho. A partir do PHP 7.4.0, foi adicionado suporte a covariância e contravariância completas.

Covariância permite que um método filho retorne um tipo mais específico que o tipo de retorno de seu método pai. Enquanto que a contravariância permite a um parâmetro ter um tipo menos específico em um método filho, em relação ao método pai.

Uma declaração de tipo é considerada mais específica nos seguintes casos:

Uma classe de tipo é considerada menos específica se o oposto for verdadeiro.

Covariância

Para ilustrar como uma variância funciona, uma classe pai abstrata simples, Animal é criada. Animal será estendida a classes filhas, Cat e 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";
    }
}

Note que não há nenhum método que retorne valores neste exemplo. Algumas factories serão adicionadas para retornar um novo objeto das classes Animal, Cat or Dog.

<?php

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

class CatShelter implements AnimalShelter
{
    public function adopt(string $name): Cat // em vez de retornar o tipo Animal, pode retornar o tipo Cat
    {
        return new Cat($name);
    }
}

class DogShelter implements AnimalShelter
{
    public function adopt(string $name): Dog // em vez de retornar o tipo Animal, pode retornar o tipo Dog
    {
        return new Dog($name);
    }
}

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

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

O exemplo acima produzirá:

Ricky meows
Mavrick barks

Contravariância

Continuando com o exemplo anterior com as classes Animal, Cat e Dog, duas classes chamadas Food e AnimalFood serão incluídas, e um método eat(AnimalFood $food) é adicionado à classe abstrata 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);
    }
}

Para ver o comportamento da contravariância, o método eat é substituído na classe Dog para permitir qualquer objeto do tipo Food. A classe Cat permanece inalterada.

<?php

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

O próximo exemplo irá mostrar o comportamento da contravariância.

<?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);

O exemplo acima produzirá:

Ricky eats AnimalFood
Mavrick eats Food

Mas o que acontece se $kitty tentar comer (eat()) a $banana?

$kitty->eat($banana);

O exemplo acima produzirá:

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