HardТеория8 min

Ковариантность и контравариантность

Covariant return types, contravariant parameters, LSP, таблица правил variance

Определения

Variance (вариантность) описывает, как подтипы контейнеров связаны с подтипами их содержимого. В PHP variance применяется к return types и parameter types при наследовании.

Термин Направление Применение Правило
Ковариантность Более специфичный Return types Дочерний может вернуть более конкретный тип
Контравариантность Более общий Parameter types Дочерний может принять более широкий тип
Инвариантность Точное совпадение Property types Тип должен совпадать точно
<?php
declare(strict_types=1);

class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {}

// Covariant: return type can be more specific
interface AnimalShelter
{
    public function adopt(): Animal;
}

class DogShelter implements AnimalShelter
{
    public function adopt(): Dog  // Dog is more specific than Animal — OK
    {
        return new Dog();
    }
}

// Contravariant: parameter type can be more general
interface DogTrainer
{
    public function train(Dog $dog): void;
}

class AnimalTrainer implements DogTrainer
{
    public function train(Animal $animal): void  // Animal is more general — OK
    {
        // Can train any animal, including dogs
    }
}

Ковариантность (Return Types)

Дочерний класс может возвращать более специфичный (более узкий) тип, чем родительский.

<?php
declare(strict_types=1);

// Hierarchy: Animal → Cat → Kitten
class Animal
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}

class Cat extends Animal
{
    public function purr(): string
    {
        return "{$this->name} purrs";
    }
}

class Kitten extends Cat
{
    public function play(): string
    {
        return "{$this->name} plays";
    }
}

// Factory hierarchy with covariant returns
interface AnimalFactory
{
    public function create(string $name): Animal;
}

class CatFactory implements AnimalFactory
{
    public function create(string $name): Cat  // More specific — covariant
    {
        return new Cat($name);
    }
}

class KittenFactory extends CatFactory
{
    public function create(string $name): Kitten  // Even more specific — OK
    {
        return new Kitten($name);
    }
}

// Why is this safe?
// Anyone expecting AnimalFactory gets an Animal
// CatFactory returns Cat which IS an Animal ✓
// KittenFactory returns Kitten which IS a Cat which IS an Animal ✓

$factory = new KittenFactory();
$animal = $factory->create('Whiskers');  // Returns Kitten
echo $animal->purr();   // "Whiskers purrs" — Cat method works
echo $animal->play();   // "Whiskers plays" — Kitten method works

Ковариантность с Union Types

<?php
declare(strict_types=1);

interface Processor
{
    public function process(): int|string|null;
}

// Narrower union — covariant (removing types from union)
class IntProcessor implements Processor
{
    public function process(): int  // int is subset of int|string|null
    {
        return 42;
    }
}

class StringProcessor implements Processor
{
    public function process(): string  // string is subset of int|string|null
    {
        return 'result';
    }
}

// Also valid:
class IntOrStringProcessor implements Processor
{
    public function process(): int|string  // Still subset (removed null)
    {
        return 42;
    }
}

// INVALID — wider return is NOT covariant:
// class BrokenProcessor implements Processor
// {
//     public function process(): int|string|float|null  // Added float — ERROR!
//     {
//         return 3.14;
//     }
// }

Ковариантность с nullable

<?php
declare(strict_types=1);

interface Repository
{
    public function find(int $id): ?Animal;  // Animal|null
}

class CatRepository implements Repository
{
    // Removing null — more specific (covariant)
    public function find(int $id): Cat  // Cat (no null) — OK
    {
        return new Cat("Cat #{$id}");
    }
}

class StrictRepository implements Repository
{
    // Keeping nullable but narrowing the type — also OK
    public function find(int $id): ?Cat  // Cat|null — OK
    {
        return $id > 0 ? new Cat("Cat #{$id}") : null;
    }
}

Контравариантность (Parameter Types)

Дочерний класс может принимать более общий (более широкий) тип параметра, чем родительский.

<?php
declare(strict_types=1);

interface CatFeeder
{
    public function feed(Cat $cat): void;
}

// Accepts more general type — contravariant
class AnimalFeeder implements CatFeeder
{
    public function feed(Animal $animal): void  // Animal is wider than Cat — OK
    {
        echo "Feeding {$animal->name}\n";
    }
}

// Why is this safe?
// Anyone calling CatFeeder::feed() passes a Cat
// AnimalFeeder::feed() accepts any Animal, so Cat is fine ✓

$feeder = new AnimalFeeder();
$feeder->feed(new Cat('Whiskers'));  // OK — Cat IS an Animal

Контравариантность с Union Types

<?php
declare(strict_types=1);

interface IntHandler
{
    public function handle(int $value): void;
}

// Wider parameter — contravariant (adding types to union)
class FlexibleHandler implements IntHandler
{
    public function handle(int|string $value): void  // Added string — OK
    {
        echo "Handling: {$value}\n";
    }
}

// Even wider
class UniversalHandler implements IntHandler
{
    public function handle(mixed $value): void  // mixed accepts everything — OK
    {
        echo "Handling: " . var_export($value, true) . "\n";
    }
}

// INVALID — narrower parameter is NOT contravariant:
// class BrokenHandler implements IntHandler
// {
//     public function handle(int $value, string $extra): void  // Adding required params — ERROR
//     {
//     }
// }

Контравариантность с nullable

<?php
declare(strict_types=1);

interface StrictService
{
    public function process(int $value): void;
}

// Adding nullable — wider parameter (contravariant)
class FlexibleService implements StrictService
{
    public function process(?int $value): void  // Accepts null too — OK
    {
        $value ??= 0;
        echo "Processing: {$value}\n";
    }
}

Принцип подстановки Лисков (LSP)

Variance правила в PHP реализуют Liskov Substitution Principle: подтип должен быть заменяем базовым типом без нарушения корректности программы.

<?php
declare(strict_types=1);

// LSP-compliant hierarchy
interface Collection
{
    public function add(Animal $item): void;
    public function first(): Animal;
}

class CatCollection implements Collection
{
    /** @var list<Cat> */
    private array $items = [];

    // Contravariant parameter: accepts wider type — but this is WRONG for Collection!
    // We cannot accept any Animal because internally we store Cats
    // This shows the tension between variance and practical design

    // In practice, use the SAME type:
    public function add(Animal $item): void  // Same as interface — invariant
    {
        // Type check at runtime
        if (!$item instanceof Cat) {
            throw new InvalidArgumentException('Only cats allowed');
        }
        $this->items[] = $item;
    }

    public function first(): Cat  // Covariant return — always a Cat
    {
        return $this->items[0] ?? throw new UnderflowException('Empty');
    }
}

// Better design: use generics via PHPStan
/**
 * @template T of Animal
 */
interface TypedCollection
{
    /** @param T $item */
    public function add(Animal $item): void;

    /** @return T */
    public function first(): Animal;
}

/**
 * @implements TypedCollection<Cat>
 */
class TypeSafeCatCollection implements TypedCollection
{
    /** @var list<Cat> */
    private array $items = [];

    public function add(Animal $item): void
    {
        assert($item instanceof Cat);
        $this->items[] = $item;
    }

    public function first(): Cat
    {
        return $this->items[0] ?? throw new UnderflowException('Empty');
    }
}

LSP нарушения

<?php
declare(strict_types=1);

// WRONG: violates LSP — child is more restrictive
class Rectangle
{
    public function __construct(
        protected float $width,
        protected float $height,
    ) {
    }

    public function setWidth(float $width): void
    {
        $this->width = $width;
    }

    public function setHeight(float $height): void
    {
        $this->height = $height;
    }

    public function area(): float
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function setWidth(float $width): void
    {
        // Violates LSP: changes semantics of setWidth
        $this->width = $width;
        $this->height = $width;  // Side effect!
    }

    public function setHeight(float $height): void
    {
        $this->width = $height;  // Side effect!
        $this->height = $height;
    }
}

// This function expects Rectangle behavior but Square breaks it:
function doubleWidth(Rectangle $rect): float
{
    $rect->setWidth($rect->area() / $rect->area() * 20);
    $rect->setHeight(10);
    return $rect->area();  // Expects 200 for Rectangle
}

// Better: use readonly value objects
final readonly class ImmutableRectangle
{
    public function __construct(
        public float $width,
        public float $height,
    ) {
    }

    public function area(): float
    {
        return $this->width * $this->height;
    }

    public function withWidth(float $width): self
    {
        return new self($width, $this->height);
    }
}

Инвариантность свойств

Свойства в PHP инвариантны -- тип должен совпадать точно.

<?php
declare(strict_types=1);

class ParentClass
{
    public Animal $pet;
}

// INVALID — property types must be EXACTLY the same
// class ChildClass extends ParentClass
// {
//     public Cat $pet;     // Fatal error! Cannot narrow
//     public mixed $pet;   // Fatal error! Cannot widen
// }

class ValidChild extends ParentClass
{
    public Animal $pet;  // Exact same type — OK
}

// WHY invariant? Because properties are both read AND written:
// - Reading: needs covariance (could be Cat when Animal expected — fine)
// - Writing: needs contravariance (could assign Animal when Cat expected — broken)
// - Both = invariant (must be exact)

PHP 7.4+ Variance Support

<?php
declare(strict_types=1);

// PHP 7.4 introduced covariant returns and contravariant parameters

// BEFORE PHP 7.4 — could only use EXACT same types or no type:
// interface OldInterface {
//     public function create(): Animal;
// }
// class OldImpl implements OldInterface {
//     public function create(): Dog { ... }  // Fatal error in PHP 7.3!
// }

// PHP 7.4+ — variance works
interface ModernInterface
{
    public function create(): Animal;
    public function handle(Dog $dog): void;
}

class ModernImpl implements ModernInterface
{
    public function create(): Puppy     // Covariant return ✓
    {
        return new Puppy('Rex');
    }

    public function handle(Animal $animal): void  // Contravariant param ✓
    {
        echo "Handling {$animal->name}\n";
    }
}

Практические примеры

Repository Pattern

<?php
declare(strict_types=1);

interface Entity
{
    public function getId(): int;
}

class User implements Entity
{
    public function __construct(
        private readonly int $id,
        public readonly string $name,
        public readonly string $email,
    ) {
    }

    public function getId(): int
    {
        return $this->id;
    }
}

class Admin extends User
{
    public function __construct(
        int $id,
        string $name,
        string $email,
        public readonly string $role = 'admin',
    ) {
        parent::__construct($id, $name, $email);
    }
}

/**
 * @template T of Entity
 */
interface RepositoryInterface
{
    /** @return T|null */
    public function findById(int $id): ?Entity;

    /** @param T $entity */
    public function save(Entity $entity): void;

    /** @return list<T> */
    public function findAll(): array;
}

/**
 * @implements RepositoryInterface<User>
 */
class UserRepository implements RepositoryInterface
{
    /** @var array<int, User> */
    private array $storage = [];

    public function findById(int $id): ?User  // Covariant: User instead of Entity
    {
        return $this->storage[$id] ?? null;
    }

    public function save(Entity $entity): void  // Same type (invariant for param)
    {
        assert($entity instanceof User);
        $this->storage[$entity->getId()] = $entity;
    }

    /** @return list<User> */
    public function findAll(): array  // Covariant: list<User> instead of list<Entity>
    {
        return array_values($this->storage);
    }
}

/**
 * @implements RepositoryInterface<Admin>
 */
class AdminRepository implements RepositoryInterface
{
    /** @var array<int, Admin> */
    private array $storage = [];

    public function findById(int $id): ?Admin  // Even more specific
    {
        return $this->storage[$id] ?? null;
    }

    public function save(Entity $entity): void
    {
        assert($entity instanceof Admin);
        $this->storage[$entity->getId()] = $entity;
    }

    /** @return list<Admin> */
    public function findAll(): array
    {
        return array_values($this->storage);
    }
}

Builder Pattern с Fluent Interface

<?php
declare(strict_types=1);

class QueryBuilder
{
    protected string $table = '';
    protected array $conditions = [];
    protected ?int $limit = null;

    public function from(string $table): static
    {
        $this->table = $table;
        return $this;
    }

    public function where(string $condition): static
    {
        $this->conditions[] = $condition;
        return $this;
    }

    public function limit(int $limit): static
    {
        $this->limit = $limit;
        return $this;
    }

    public function toSql(): string
    {
        $sql = "SELECT * FROM {$this->table}";

        if ($this->conditions !== []) {
            $sql .= ' WHERE ' . implode(' AND ', $this->conditions);
        }

        if ($this->limit !== null) {
            $sql .= " LIMIT {$this->limit}";
        }

        return $sql;
    }
}

class SoftDeleteQueryBuilder extends QueryBuilder
{
    public function withTrashed(): static  // Returns static (SoftDeleteQueryBuilder)
    {
        return $this;
    }

    public function onlyTrashed(): static
    {
        return $this->where('deleted_at IS NOT NULL');
    }
}

// Chaining works correctly thanks to `static` return type
$sql = (new SoftDeleteQueryBuilder())
    ->from('users')                    // Returns SoftDeleteQueryBuilder
    ->where('age > 18')               // Returns SoftDeleteQueryBuilder
    ->onlyTrashed()                   // Returns SoftDeleteQueryBuilder
    ->limit(10)                       // Returns SoftDeleteQueryBuilder
    ->toSql();

echo $sql;
// SELECT * FROM users WHERE age > 18 AND deleted_at IS NOT NULL LIMIT 10

Полная таблица правил Variance

RETURN TYPE — COVARIANT (дочерний возвращает более специфичный):
─────────────────────────────────────────────────────────────
Parent Return      │ Valid Child Return
───────────────────┼──────────────────────────────────────
Animal             │ Animal, Cat, Dog, Puppy
?Animal            │ ?Animal, Animal, Cat, ?Cat
int|string         │ int|string, int, string
int|string|null    │ int|string|null, int|string, int, ?string
mixed              │ mixed, int, string, Animal, anything
void               │ void (only)
static             │ static (resolves to actual class)
self               │ self (resolves to defining class)
(no type)          │ anything (adding type is OK)

PARAMETER TYPE — CONTRAVARIANT (дочерний принимает более общий):
─────────────────────────────────────────────────────────────
Parent Param       │ Valid Child Param
───────────────────┼──────────────────────────────────────
Cat                │ Cat, Animal
Dog                │ Dog, Animal
int                │ int, int|string, int|float, mixed
?int               │ ?int, int|string|null, mixed
int|string         │ int|string, int|string|float, mixed
(no type)          │ (no type) only

PROPERTY TYPE — INVARIANT (должен совпадать):
─────────────────────────────────────────────────────────────
Parent Property    │ Valid Child Property
───────────────────┼──────────────────────────────────────
int                │ int (only)
?string            │ ?string (only)
Animal             │ Animal (only)

Проверь себя

5 из 11

Что нарушает LSP (Liskov Substitution Principle)?

Почему свойства инвариантны?

Какой return type использовать для Fluent Interface в иерархии классов?

С какой версии PHP поддерживается covariant return types?

Что такое контравариантность параметров?