In PHP 7.4, covariance and contravariance were introduced. 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
.
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";
}
}
You'll noticed 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
. This is where we can see covariance in action!
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();
Outputs
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.
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);
}
}
Now, cats typically are picky eaters. They will eat a specific type of food, and that's it. Dogs, on the other hand, will eat just about anything. Thus, we're going to override the eat
method in the Dog
class to allow any Food
type object. The Cat
class remains unchanged.
class Dog extends Animal
{
public function eat(Food $food) {
echo $this->name . " nom noms " . get_class($food);
}
}
With that change, we can see how contravariance works.
$kitty = (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo "\n";
$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);
Outputs
Ricky nom noms AnimalFood
Mavrick nom noms Food
But what happens if $kitty
tries to eat
the $banana
?
$kitty->eat($banana);
Outputs
Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given