Prior to PHP 7.4.0, class methods support mainly invariant parameter types and invariant return types. This means that if a method within a parent class has a parameter type or return type of T, any child method with corresponding parameter type or return type must also be type T.
As of PHP 7.4.0, covariance and contravariance is supported. While these concepts may sound confusing, in practice they're rather simple, and extremely useful to object-oriented programming.
Covariance allows a child's method to return a more specific type than the return type of its parent's method. This is better illustrated with an example.
We'll start with a simple abstract parent class, Animal, which is extended by children classes, Cat, and 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 that there aren't any methods which return values in this example. We will build upon these classes with a few factories which return a new object of class type Animal, Cat, or Dog. Covariance will come into play in the next example.
<?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();
The above example will output:
Ricky meows Mavrick barks
Contravariance, on the other hand, allows a parameter type to be less specific in a child method, than that of its parent. Continuing with our previous example with the classes Animal, Cat, and Dog, we're adding a class called Food and AnimalFood, and adding a method eat(AnimalFood $food) to our Animal abstract class.
<?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 . " nom noms " . get_class($food);
}
}
In order to see the behavior of contravariance, we will override the eat method in the Dog class to allow any Food type object. The Cat class remains unchanged.
<?php
class Dog extends Animal
{
public function eat(Food $food) {
echo $this->name . " nom noms " . get_class($food);
}
}
Now, we can see how contravariance works.
<?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);
The above example will output:
Ricky nom noms AnimalFood Mavrick nom noms Food
But what happens if $kitty tries to eat the $banana?
$kitty->eat($banana);
The above example will output:
Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given