Tracks
/
PHP
PHP
/
Syllabus
/
Covariance and Contravariance
Co

Covariance and Contravariance in PHP

0 exercises

About Covariance and Contravariance

Covariance and Contravariance

When utilizing union types you must be aware of the concepts of covariance and contravariance. These are explained in the documentation but these examples may help.

Suppose we have a Calculator class utilizing a function like the above example:

class Calculator
{
    public function add(int|float $x, int|float $y): int|float
    {
        return $x + $y;
    }
}

If you were to extend the class, you are allowed to change the parameter types and return types, but only according to the following rules.

First, parameter types may be "wider", meaning they can accept more types than the base class. For example, this is allowed:

class StringCalc extends Foo
{
    public function add(float|int|string $x, float|int|string $y): int|float
    {
        if (!is_numeric($x) || !is_numeric($y)) {
            throw new \InvalidArgumentException('$x and $y must be numeric');
        }
        return $x + $y;
    }
}

We could substitute StringCalc in place of an original Calculator and everything would still work. The caller would still be able to send in the float or int it originally could, and the return type would still be the expected int or float.

Similarly, you can narrow the return type, meaning make it more specific by removing types from the union. This example takes advantage of this rule:

class IntCalc extends Calculator
{
    public function add(float|int $x, float|int $y) : int
    {
        return (int)($x + $y);
    }
}

Since the IntCalc still accepts int and float it can still substitute for an original Calculator with no problems.

To read more about what is and isn't allowed, you can refer to the Liskov Substitution Principal (LSP).

For an example of what would not be allowed, take a look at InvalidCalc below:

class InvalidCalc extends Calculator
{
    public function add(int $x, int $y) : int|float|string
    {
        // Declaration must be compatible with Calculator->add(x: float|int, y: float|int)

        // Return type declaration must be compatible with Calculator->add(x: float|int, y: float|int) : float|int
    }
}

The InvalidCalc class will not compile and has two problems. By reducing what is allowed as a parameter, InvalidCalc could not stand in for the original Calculator because it no longer accepts float. Additionally, it now could return a string which is also not within the allowed return types of the original class.

Edit via GitHub The link opens in a new window or tab