## Covariance et Contravariance

En PHP 7.2.0, la contravariance partielle a été introduite en supprimant les restrictions de type sur les paramètres d'une méthode enfant. À partir de PHP 7.4.0, la covariance et la contravariance complètes ont été ajoutées.

La covariance permet à une méthode enfant de retourner un type plus spécifique que le type de retour de sa méthode parente. La contravariance permet à un type de paramètre d'être moins spécifique dans une méthode enfant que dans celui de la méthode parente.

Une déclaration de type est considérée comme plus spécifique dans le cas suivant :

- Un type est retiré d'un type union
- Un type est ajouté à un type d'intersection
- Un type de classe est transformé en un type de classe enfant
- iterable est remplacé par array ou Traversable

Un type de classe est considéré moins spécifique si l'inverse est vrai.

### Covariance

Pour illustrer le fonctionnement de la covariance, une simple classe parente abstraite, Animal est créée. Animal sera étendu par des classes enfants, Cat et 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";
        }
    }

Il est à noter qu'il n'y a pas de méthodes qui renvoient des valeurs dans cet exemple. Quelques fabriques seront ajoutées et renverront un nouvel objet de classe de type Animal, Cat, ou Dog.

    <?php

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

    class CatShelter implements AnimalShelter
    {
        public function adopt(string $name): Cat // au lieu de retourner le type de classe Animal, on peut retourner le type de classe Cat
        {
            return new Cat($name);
        }
    }

    class DogShelter implements AnimalShelter
    {
        public function adopt(string $name): Dog // au lieu de retourner le type de classe Animal, on peut retourner le type de classe Dog
        {
            return new Dog($name);
        }
    }

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

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

L'exemple ci-dessus va afficher :

    Ricky meows
    Mavrick barks

### Contravariance

En reprenant l'exemple précédent avec les classes Animal, Cat et Dog, une classe appelée Food et AnimalFood seront incluses, et une méthode eat(AnimalFood $food) est ajoutée à la classe abstraite 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);
        }
    }

Afin de voir le comportement de la contravariance, la méthode eat est surchargée dans la classe Dog afin d'autoriser n'importe quel objet de type Food. La classe Cat reste inchangée.

    <?php

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

L'exemple suivant montre le comportement de la contravariance.

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

L'exemple ci-dessus va afficher :

    Ricky eats AnimalFood
    Mavrick eats Food

Mais que se passe-t-il si $kitty essaie de manger (eat()) la banane ($banana) ?

    $kitty->eat($banana);

L'exemple ci-dessus va afficher :

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

### Variation de type des propriétés

Par défaut, les propriétés ne sont ni covariantes ni contravariantes, donc invariantes. Autrement dit, leur type ne peut pas du tout changer dans une classe enfant. La raison en est que les opérations "get" doivent être covariantes, et les opérations "set" doivent être contravariantes. La seule façon pour une propriété de satisfaire ces deux exigences est d'être invariante.

À partir de PHP 8.4.0, avec l'ajout des propriétés abstraites (sur une interface ou une classe abstraite) et propriétés virtuelles, il est possible de déclarer une propriété qui n'a qu'une opération "get" ou "set". En conséquence, les propriétés abstraites ou les propriétés virtuelles qui n'ont que l'opération "get" requise peuvent être covariantes. De même, une propriété abstraite ou une propriété virtuelle qui n'a que l'opération "set" requise peut être contravariante.

Cependant, une fois qu'une propriété a à la fois une opération "get" et "set", elle n'est plus covariante ni contravariante pour une extension future. Autrement dit, elle devient désormais invariante.

Exemple #1 Variation du type des propriétés

    <?php
    class Animal {}
    class Dog extends Animal {}
    class Poodle extends Dog {}

    interface PetOwner
    {
        // Seule l'opération "get" est requise, donc cela peut être covariant.
        public Animal $pet { get; }
    }

    class DogOwner implements PetOwner
    {
        // Cela peut être un type plus restrictif, car le côté "get"
        // retourne toujours un Animal. Cependant, en tant que propriété native,
        // les enfants de cette classe ne peuvent plus changer le type.
        public Dog $pet;
    }

    class PoodleOwner extends DogOwner
    {
        // Ceci N'EST PAS AUTORISÉ, car DogOwner::$pet a à la fois
        // les opérations "get" et "set" définies et requises.
        public Poodle $pet;
    }
    ?>

`↑` and `↓` to navigate • `Enter` to select • `Esc` to close • `/` to open