Generics в PHP: Типизация, которой вам не хватало

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему PHP-разработчики мечтают о дженериках

Если вы когда-нибудь писали на Java, C# или TypeScript, а потом возвращались к PHP, вы наверняка испытывали лёгкое разочарование. В тех языках есть элегантная система обобщённых типов (Generics), которая позволяет создавать гибкие, но при этом безопасные с точки зрения типов структуры данных. В PHP же долгое время царила "типизационная анархия": массивы могли содержать что угодно, а IDE подсвечивала только самые очевидные ошибки.

Но времена меняются. Начиная с PHP 7.0 и особенно в эпоху PHP 8.0/8.1/8.2, сообщество нашло способ использовать Generics в PHP. Да, на уровне синтаксиса языка их до сих пор нет (и, скорее всего, не будет в ближайшие годы), но это не значит, что вы не можете пользоваться всеми преимуществами обобщённого программирования уже сегодня.

В этой статье мы разберём, как работает типизация в PHP с Generics, какие инструменты для этого существуют (PHPStan, Psalm), и как писать код, который будет одновременно гибким и безопасным. Пристегнитесь — будет жарко!

Что такое Generics и зачем они нужны в PHP?

Generics (обобщённые типы) — это механизм, который позволяет параметризовать классы, интерфейсы и функции типами. Вместо того чтобы писать отдельную реализацию для каждого типа данных, вы создаёте одну универсальную, а конкретный тип подставляете при использовании.

Представьте, что вы пишете класс для очереди (Queue). Без дженериков вы можете хранить в ней только один тип данных или использовать mixed (что убивает всю типизацию). С Generics вы можете сказать: "Это очередь целых чисел" или "Это очередь объектов User".

Проблема "сырой" типизации

Посмотрите на этот типичный PHP-код:

<?php

class Collection {
    private array $items = [];

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

    public function get(int $index) {
        return $this->items[$index] ?? null;
    }
}

$users = new Collection();
$users->add(new User('Alice'));
$users->add(new User('Bob'));
$users->add('это строка, а не пользователь'); // Ошибка! Но PHP промолчит

$user = $users->get(2); // Вернёт строку, IDE ничего не заподозрит
$user->getName(); // Фатальная ошибка во время выполнения

Это классическая проблема PHP-разработчика: отсутствие контроля типов на этапе написания кода. Вы узнаёте об ошибке только в рантайме, когда пользователь уже нажал на кнопку. Generics решают эту проблему на корню.

Generics в PHP: Как это работает на практике

Поскольку нативный синтаксис Generics в PHP отсутствует, мы используем аннотации в PHPDoc, которые обрабатываются статическими анализаторами — PHPStan и Psalm. Эти инструменты проверяют код на соответствие объявленным типам, фактически эмулируя работу дженериков.

Базовый синтаксис: шаблонные типы

Предположим, мы хотим сделать нашу коллекцию типобезопасной. Вот как это выглядит с использованием Generics через PHPDoc:

<?php

/**
 * @template T
 */
class GenericCollection {
    /** @var array<int, T> */
    private array $items = [];

    /**
     * @param T $item
     */
    public function add($item): void {
        $this->items[] = $item;
    }

    /**
     * @param int $index
     * @return T|null
     */
    public function get(int $index) {
        return $this->items[$index] ?? null;
    }
}

// Теперь используем:
/** @var GenericCollection<User> $users */
$users = new GenericCollection();
$users->add(new User('Alice'));
$users->add('строка'); // PHPStan выдаст ошибку: Expected type User, got string

$user = $users->get(0); // PHPStan знает, что это User
echo $user->getName(); // IDE подсказывает метод

Магия в том, что анализатор "запоминает": если вы создали GenericCollection<User>, то все методы add принимают только User, а get возвращает User. Ключевые слова @template и @param/@return с указанием типа T — это основа Generics в PHP.

Ограничения типов (Type Constraints)

Иногда нужно, чтобы T был не любым типом, а, например, только объектами, реализующими интерфейс EntityInterface. Для этого используется конструкция @template T of SomeType:

<?php

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

/**
 * @template T of EntityInterface
 */
class EntityRepository {
    /** @var array<int, T> */
    private array $entities = [];

    /**
     * @param T $entity
     */
    public function save($entity): void {
        $this->entities[$entity->getId()] = $entity;
    }

    /**
     * @param int $id
     * @return T|null
     */
    public function find(int $id) {
        return $this->entities[$id] ?? null;
    }
}

class User implements EntityInterface {
    public function getId(): int { return 1; }
    public function getName(): string { return 'Alice'; }
}

/** @var EntityRepository<User> $userRepo */
$userRepo = new EntityRepository();
$userRepo->save(new User());
$user = $userRepo->find(1); // Тип — User, можно вызывать getName()

Это мощный инструмент: вы говорите анализатору: "T может быть только EntityInterface", и он строго следит за этим.

Продвинутые техники: Ковариантность и контравариантность

Когда вы начинаете активно использовать Generics в PHP, рано или поздно столкнётесь с вопросами наследования типов. Можно ли передать Collection<Dog> туда, где ожидается Collection<Animal>? Ответ зависит от того, как вы объявите ваш generic.

Инвариантность (по умолчанию)

По умолчанию PHPStan и Psalm считают дженерики инвариантными. Это значит, что Collection<Dog> — это совсем не то же самое, что Collection<Animal>. И это правильно: если бы вы могли положить Cat в Collection<Dog> через Collection<Animal>, была бы катастрофа.

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

Если ваш generic-класс только отдаёт значения типа T (например, итератор или провайдер), вы можете пометить его как ковариантный с помощью @template-covariant T. Тогда Provider<Dog> будет считаться подтипом Provider<Animal>.

<?php

/**
 * @template-covariant T
 */
class Result {
    /**
     * @param T $value
     */
    public function __construct(private $value) {}

    /**
     * @return T
     */
    public function get                

Блоги

Рекомендаций по книгам