Определения
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)