Covariance, Contravariance in PHP

Covariance and contravariance — are concepts related to data typing and describe the compatibility of types with respect to each other. This is a type-safety mechanism in polymorphism (different implementations of one contract/interface).

  • Covariance — "narrows" the type - allows the use of a more specific type than that defined in the parent class - specifies the return type.

  • Contravariance — "broadens" the type - on the contrary, allows the use of a more general type. Lowers the requirements for input parameters.

Example of covariance:

class Parent {
	public function method(): int|float {}
}

// Correct: Covariance is respected
class Child extends Parent {
	public function method(): int {}
}

// Incorrect: Covariance is violated
class Child extends Parent {
	public function method(): string {} // FATAL ERROR !!!
}

Example of contravariance:

class Parent {
	public function method( int|float $param ) {}
}

// Correct: Contravariance is respected
class Child extends Parent {
	public function method( int|float|string $param ) {}
}

// Incorrect: Contravariance is violated
class Child extends Parent {
	public function method( int $param ) {} // FATAL ERROR !!!
}

Rules of Covariance and Contravariance in PHP

Covariance and contravariance are provided in PHP at the language level. Partial support was added in PHP 7.2, and full support in PHP 7.4. More details.

The rules of covariance and contravariance in PHP impose restrictions on the use of one data type instead of another in the code. That is, these rules define how the data types of return values and input parameters of methods can change in subclasses.

When a class uses an interface method or overrides a method that has already been defined in the parent class, the overridden method must comply with the variance rules, otherwise the code will throw a fatal error.

In other words, the overridden method must be compatible with the method that was defined earlier.

These concepts are directly related to one of the principles of SOLID: LSP (Liskov Substitution Principle). According to this principle, we should use child classes instead of the base class, so that the child class can do everything that the base class can do. For example, when overloading a method, the new method must be able to do everything that the original method does (accept the same parameters, return compatible data/types).

In OOP, covariance and contravariance are directly related to the concept of "polymorphism," as they allow the use of objects of different subtypes instead of objects of the base type, which is one of the principles of polymorphism - different implementations of one contract.

Why is this needed?

This is needed so that we can substitute the base class with a derived one and at the same time the current code does not "notice" such substitution and continues to work as before. This adheres to the Liskov substitution principle. This guarantees backward compatibility.

Covariance Constraints

Are needed to ensure that when substituting a "new" method instead of the base one, a type will be returned that the current code can work with.

If this rule is violated, then a method that previously returned one type will now return another, which the current code cannot work with. As a result, something unforeseen (unpleasant) may happen.

Contravariance Constraints

Are needed to ensure that the "new" method can accept and process all parameters that the base method accepted. Thus, if the current code uses this method, when replacing the class (method), one can be sure that the "new" method will not break anything.

At the same time, the "new" method can "understand" additional types that the current code knows nothing about.

If this rule is violated and the "new" method cannot handle one of the types that the "old" method could handle, then when using the "new" method by the current code, the program may crash with an error.

Abandoning Variance Rules

On the other hand, if types for incoming and outgoing data of methods are not specified at all, then in situations where an unforeseen data type comes in, the program will continue its work without throwing any errors. And it is good if later in the code the program "crashes" with an error. But the program may not crash, resulting in the code executing incorrectly and potentially leading to unfortunate consequences.

Covariance

Allows the child method to return the specified or a more specific type than the type defined in the parent method.

Example:

class Parent {
	public function method(): int|float|string { ... }
}

class Child extends Parent {
	public function method(): int { ... }
}

Example:

class Parent {
	public function method(): Base { ... }
}

class Child extends Parent {
	public function method(): Child { ... }
}

Example:

class Parent {
	public function method(): iterable { ... }
}

class Child extends Parent {
	public function method(): array { ... }
}

Types are considered more specific in the following cases:

// If the union of types is removed
: int|float|string  >> : int

// If intersection types are added
: Iterator          >> : Iterator & Countable

// When the type changes to a subtype
: Base              >> : Child
: iterable          >> : array
: iterable          >> : Traversable
Hierarchy of iterable objects in PHP

If we try to extend the return type:

: int|float  >> : int|float|string|bool
: int|float  >> : mixed
: Iterator   >> : iterable

Then we will receive a warning in the IDE and a fatal error at runtime (when running the code).

class Father {
	public function method(): int {  }
}

class Child extends Father {
	public function method(): int|string {  }
}

// Compile Error:
// Declaration of Child::method(): string|int
// must be compatible with Father::method(): int

Contravariance

Lowers the requirements for input data, allowing the parameter types of the method to be broadened. Contravariance allows the method parameter to be more general than specified in the parent class.

Example:

class Parent {
	public function method( int|float $param ): void {}
}

class Child extends Parent {
	public function method( int|float|string $param ): void {}
}

Example:

class Parent {
	public function method( Child $param ): void {}
}

class Child extends Parent {
	public function method( Base $param ): void {}
}

Example:

class Parent {
	public function method( Traversable $param ): void {}
}

class Child extends Parent {
	public function method( iterable $param ): void {}
}

The child method must accept all the types that the parent method can work with, and may also work with additional types if necessary.

Thus, the child method can work with what the parent method cannot (contravariance), but it must return what is expected from the parent method (covariance). That is, "outward" the method is limited by the parent type, while "inward" it is unrestricted (extendable).

Invariance

Invariance is a situation where any inheritance is prohibited and the exact type specified must be used. That is, there is no possibility to use other type variants - only the specified one.

In other words — it is "constancy," immutability, permanence, independence from any conditions. For example, the speed of light is invariant.

Invariance in PHP

PHP does not provide built-in support for invariance at the language level.

However, various techniques and practices can be used to ensure invariance in the code. For example, condition checks can be used to ensure compliance with invariance.