Support the ongoing development of Laravel.io →
Article Hero Image

Covariance and Contravariance in PHP

8 Apr, 2025 10 min read

Photo by Rishabh Dharmani on Unsplash

Introduction

"Covariance" and "contravariance" are two terms I didn't know existed until about a year ago. But when I learned what they were, I realised I'd been following these concepts for years without knowing their names. So I did some research to understand them better and had a few light bulb moments of "Oh, so that's why my code didn't work a few years ago!".

In this article, I want to pass on what I've learnt about covariance and contravariance and how they apply to PHP.

This is a topic that can get a little mind-bending (at least it does for me), so I'll try and keep it as short and snappy as possible. At the end, I'll leave a "cheat sheet" that summarises the key points so you can keep referring back to it.

Hopefully, by the end of this article, you will have a better understanding of covariance and contravariance in PHP.

Quick Definitions

Before we dive into the details and code examples, let me quickly define covariance and contravariance:

  • Covariance: Making something more specific
  • Contravariance: Making something less specific

Now let's dive in and see how these concepts apply to PHP.

Covariance

At its core, covariance is about a child class using something more specific than its parent class. Let's take a look at what this means.

Covariance in Return Types (Union Types)

PHP supports covariance for return types. This means that a method in a child class can return a more specific type than the method it's overriding/implementing from the parent class/interface.

Let's take this example of a parent class that has a getFileSizeInKiloBytes method which uses a union return type that allows a float or int to be returned:

declare(strict_types=1);

namespace App\Services\Reports;

readonly class BaseReportBuilder
{
    public function getFileSizeInKiloBytes(): float|int
    {
        // ...
    }
}

Let's then say we extend this class and want to override this method. For the sake of this example, we'll assume the logic in the child class' method is different to the parent class' method. We might want to leave the return type as float|int:

declare(strict_types=1);

namespace App\Services\Reports;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function getFileSizeInKiloBytes(): float|int
    {
        // ...
    }
}

As we can see in the example above, the child class method has the same return type. There's nothing too exciting to see here, and most of your overridden methods will look like this (having matching return types).

However, if we're confident we'll always return an integer from this method, we change the return type to be more specific. For example, we could change the return type to just int:

declare(strict_types=1);

namespace App\Services\Reports;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function getFileSizeInKiloBytes(): int
    {
        // ...
    }
}

In the code example, we can see that we've made the return type more specific by changing it from float|int to just int. This is completely valid code and an example of covariance.

Covariance in Return Types (Intersection Types)

Another way to demonstrate covariance is with intersection types. Let's imagine we have two interfaces:

  • App\Interfaces\Cacheable - Can be applied to any class that can be cached.
  • App\Interfaces\Exportable - Can be applied to any class that can be exported (for example, as a CSV file report class).

Let's say we have a base report builder class with a buildReport method that returns an App\Interfaces\Exportable type:

declare(strict_types=1);

namespace App\Services\Reports;

use App\Interfaces\Exportable;

readonly class BaseReportBuilder
{
    public function buildReport(): Exportable
    {
        // ...
    }
}

We might then extend this class and want to enforce that the buildReport method returns a type that implements both the App\Interfaces\Cacheable and App\Interfaces\Exportable interfaces. We can do this by using an intersection type:

declare(strict_types=1);

namespace App\Services\Reports;

use App\Interfaces\Cacheable;
use App\Interfaces\Exportable;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function buildReport(): Exportable&Cacheable
    {
        // Build and return report...
    }
}

As we can see in the example above, we've changed the return type from Exportable to Exportable&Cacheable. This is a more specific type because it requires the returned object to implement both interfaces. This is another example of covariance as we have made the return type more specific in the child method.

Covariance in Return Types (Classes)

We can also demonstrate covariance when we're using classes as return types.

For example, let's say we have a base report class:

declare(strict_types=1);

namespace App\Services\Reports;

readonly class BaseReport
{
    // ...
}

We'll then extend this class and create a child class called App\Services\Reports\ExcelReport:

declare(strict_types=1);

namespace App\Services\Reports;

readonly class ExcelReport extends BaseReport
{
    // ...
}

Let's then say we have a base report builder class with a buildReport method that returns an App\Services\Reports\BaseReport type:

declare(strict_types=1);

namespace App\Services\Reports;

readonly class BaseReportBuilder
{
    public function buildReport(): BaseReport
    {
        // Build and return report...
    }
}

We'll then extend our base report builder class and override the buildReport method to return a more specific type, App\Services\Reports\ExcelReport:

declare(strict_types=1);

namespace App\Services\Reports;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function buildReport(): ExcelReport
    {
        // Build and return report...
    }
}

Since the App\Services\Reports\ExcelReport class extends the App\Services\Reports\BaseReport class, this is more specific than the parent class method. This is another example of covariance in PHP.

Covariance in Parameter Types

PHP doesn't support covariance for parameter types.

If you were to change the parameter type in the child class to be more specific than the parent class (e.g., float|int to int), PHP will throw an error.

Contravariance

Whereas covariance is about a child method using something more specific than its parent method, contravariance is the opposite.

Contravariance is about a child method using something less specific than its parent method. Let's take a look at what this means.

Contravariance in Return Types

PHP doesn't support contravariance for return types.

If you were to change the return type in a child class' method to be less specific than the parent class' (e.g., int to float|int), PHP will throw an error.

Contravariance in Parameter Types (Union Types)

Let's imagine our base report builder class has a setHeaders method which accepts an array as the only parameter:

declare(strict_types=1);

namespace App\Services\Reports;

readonly class BaseReportBuilder
{
    public function setHeaders(array $headers): void
    {
        // ...
    }
}

We might want to extend this class and override the setHeaders method to accept an array or Illuminate\Support\Collection as the parameter type:

declare(strict_types=1);

namespace App\Services\Reports;

use Illuminate\Support\Collection;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function setHeaders(array|Collection $headers): void
    {
        // ...
    }
}

As we can see in the example above, we've changed the parameter type from array to array|Collection. This is a less specific type because it allows for either an array or a Illuminate\Support\Collection to be passed in. This is an example of contravariance and is valid code.

Contravariance in Parameter Types (Intersection Types)

Alternatively, let's imagine our base report builder class' setHeaders method is using an intersection type and expects an argument which implements the Traversable interface and is an instance of (or extends) the Illuminate\Support\Collection class as the parameter type.

Note: You probably wouldn't pair these two together in your own code, but I've used this interface and class purely for example purposes.

The method signature may look like so:

declare(strict_types=1);

namespace App\Services\Reports;

use Illuminate\Support\Collection;
use Traversable;

readonly class BaseReportBuilder
{
    public function setHeaders(Traversable&Collection $headers): void
    {
        // ...
    }
}

In our child class, we may want to make our setHeaders method contravariant (less specific) by changing the parameter type to just Illuminate\Support\Collection:

declare(strict_types=1);

namespace App\Services\Reports;

use Illuminate\Support\Collection;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function setHeaders(Collection $headers): void
    {
        // ...
    }
}

As we can see in the example above, we've changed the parameter type from Traversable&Collection to just Collection. This is less specific and means the $headers parameter doesn't need to implement the Traversable interface. This is another example of contravariance.

Contravariance in Parameter Types (Classes)

This time, let's imagine our base report builder class' setRows method accepts an instance of Illuminate\Database\Eloquent\Collection as the parameter type.

It's worth noting that the Illuminate\Database\Eloquent\Collection class is a child class of the Illuminate\Support\Collection class.

The method signature may look like so:

declare(strict_types=1);

namespace App\Services\Reports;

use Illuminate\Database\Eloquent\Collection as EloquentCollection;

readonly class BaseReportBuilder
{
    public function setRows(EloquentCollection $rows): void
    {
        // ...
    }
}

In our child class, we may want to make our setRows method contravariant (less specific) by changing the parameter type to just Illuminate\Support\Collection:

declare(strict_types=1);

namespace App\Services\Reports;

use Illuminate\Support\Collection;

final readonly class ExcelReportBuilder extends BaseReportBuilder
{
    public function setRows(Collection $rows): void
    {
        // ...
    }
}

As we can see in the example above, we've changed the parameter type from Illuminate\Database\Eloquent\Collection to just Illuminate\Support\Collection. This is another example of contravariance as we've made the parameter type less specific.

Covariance and Contravariance in PHP Constructors

Constructors are a special case in PHP when it comes to covariance and contravariance. They are not inherited like other methods, so covariance and contravariance do not apply to them.

For example, let's say we have a parent class with a constructor that accepts a string parameter:

declare(strict_types=1);

readonly class BaseClass
{
    public function __construct(string $name)
    {
        // Constructor logic here
    }
}

Now let's say we have a child class that extends the BaseClass class but has a constructor that accepts an int parameter:

declare(strict_types=1);

readonly class ChildClass extends BaseClass
{
    public function __construct(int $id)
    {
        parent::__construct('a string');
    }
}

As we can see, the ChildClass class has a constructor that accepts an int parameter, which is different from the string parameter in the BaseClass class. Despite these differences, this is completely valid code.

Cheat Sheet

Here's a quick cheat sheet on covariance and contravariance in PHP that you can keep referring back to:

Definitions

Covariant = more specific

Contravariant = less specific

Return Types

✅ PHP supports covariant (more specific) return types.

❌ PHP does not support contravariant (less specific) return types.

Parameter Types

✅ PHP supports contravariant (less specific) parameter types.

❌ PHP does not support covariant (more specific) parameter types.

What Makes a Type More Specific?

A type declaration is considered more specific (covariant) in the following cases:

  • A type is removed from a union type (e.g., if int|float is changed to int)
  • A type is added to an intersection type (e.g., if int is changed to int&float)
  • A class type is changed to a child class type (e.g., if BaseReport is changed to ExcelReport)

What Makes a Type Less Specific?

A type declaration is considered less specific (contravariant) in the following cases:

  • A type is added to a union type (e.g., if int is changed to int|float)
  • A type is removed from an intersection type (e.g., if int&float is changed to int)
  • A class type is changed to a parent class type (e.g., if ExcelReport is changed to BaseReport)

Conclusion

In this article, we've explored covariance and contravariance in PHP. Hopefully, you now have a better understanding of what these terms mean and how they apply to PHP.

If you enjoyed reading this post, you might be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.

Or, you might want to check out my other 440+ page ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.

If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter.

Keep on building awesome stuff! 🚀

Last updated 1 week ago.

driesvints, maxoriola liked this article

2
Like this article? Let the author know and give them a clap!
ash-jc-allen (Ash Allen) I'm a freelance Laravel web developer from Preston, UK. I maintain the Ash Allen Design blog and get to work on loads of cool and exciting projects 🚀

Other articles you might like

Article Hero Image April 10th 2025 Sponsored

LarAgent: An Open-source package to Build & Manage AI Agents in Laravel

Laravel has all the right ingredients to become a strong candidate for AI development. With its eleg...

Read article
Article Hero Image April 2nd 2025

Human-Readable File Sizes in Laravel (KB, MB, GB)

Introduction There may be times when you want to display file sizes to your users in a human-readabl...

Read article
Article Hero Image March 31st 2025

Formatting and Spelling Ordinal Numbers in PHP and Laravel

Introduction There may be times when you want to convert numbers to ordinal numbers in your PHP and...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2025 Laravel.io - All rights reserved.