Generics в PHP: зачем нужны и как использовать

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

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

Начать курс

Generics в PHP: зачем нужны и как использовать

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

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

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

Основные проблемы, которые решают Generics:

  • Безопасность типов — исключают ошибки, связанные с передачей данных неправильного типа.
  • Уменьшение дублирования кода — не нужно писать отдельные классы для каждого типа.
  • Улучшение читаемости — сразу понятно, с какими типами работает функция или класс.
  • Поддержка IDE — автодополнение и анализ кода становятся точнее.

Проблема без Generics: пример на PHP

Представьте, что вам нужно создать коллекцию, которая хранит только целые числа. Без Generics вы можете написать:

class IntegerCollection {
    private array $items = [];

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

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

Всё работает, но если понадобится коллекция для строк, придётся копировать код и менять типы. А если нужна коллекция для объектов — то же самое. Это приводит к дублированию и увеличению объёма кода.

Эмуляция Generics в PHP с помощью аннотаций и PHPDoc

На данный момент PHP не поддерживает полноценные Generics на уровне синтаксиса, но их можно эмулировать с помощью PHPDoc-аннотаций и специальных библиотек (например, phpstan/phpstan или vimeo/psalm).

Пример обобщённого класса с аннотациями:

/**
 * @template T
 */
class Collection {
    private array $items = [];

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

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

Теперь при использовании класса вы можете указать конкретный тип:

/** @var Collection<int> $intCollection */
$intCollection = new Collection();
$intCollection->add(42);
$value = $intCollection->get(0); // IDE знает, что это int

Статические анализаторы (PHPStan, Psalm) проверят, что вы не передаёте строку в коллекцию целых чисел. Это даёт почти ту же безопасность, что и полноценные Generics.

Реальные примеры использования Generics в PHP

Рассмотрим несколько практических сценариев, где Generics особенно полезны.

1. Репозитории в Doctrine

В Doctrine ORM вы часто работаете с репозиториями. Generics помогают явно указать, с какой сущностью работает репозиторий:

/**
 * @template T of object
 */
interface RepositoryInterface {
    /**
     * @param int $id
     * @return T|null
     */
    public function find(int $id): ?object;
}

/**
 * @implements RepositoryInterface<User>
 */
class UserRepository implements RepositoryInterface {
    public function find(int $id): ?User {
        // логика поиска
    }
}

2. Паттерн "Стратегия" с обобщениями

Generics позволяют создавать гибкие стратегии обработки данных:

/**
 * @template TInput
 * @template TOutput
 */
interface HandlerInterface {
    /**
     * @param TInput $input
     * @return TOutput
     */
    public function handle($input);
}

/**
 * @implements HandlerInterface<string, int>
 */
class StringLengthHandler implements HandlerInterface {
    public function handle($input): int {
        return strlen($input);
    }
}

3. Собственные контейнеры и фабрики

Generics идеально подходят для создания типобезопасных контейнеров:

/**
 * @template T
 */
class Container {
    /** @var array<string, T> */
    private array $services = [];

    /**
     * @param string $id
     * @param T $service
     */
    public function set(string $id, $service): void {
        $this->services[$id] = $service;
    }

    /**
     * @param string $id
     * @return T
     * @throws InvalidArgumentException
     */
    public function get(string $id) {
        if (!isset($this->services[$id])) {
            throw new InvalidArgumentException("Service $id not found");
        }
        return $this->services[$id];
    }
}

// Использование
/** @var Container<LoggerInterface> $loggerContainer */
$loggerContainer = new Container();
$loggerContainer->set('logger', new FileLogger());
$logger = $loggerContainer->get('logger'); // Тип LoggerInterface

Будущее Generics в PHP: что нас ждёт?

На данный момент существует RFC "Generic types", который находится в стадии обсуждения. В PHP 8.1 и 8.2 появились улучшения в системе типов (перечисления, readonly-свойства, intersection types), но полноценные Generics пока не реализованы.

Однако сообщество активно использует эмуляцию через аннотации, и многие крупные проекты (Symfony, Laravel, Doctrine) уже внедрили поддержку Generics через статические анализаторы. Это позволяет:

  • Получать предупреждения об ошибках типов на этапе разработки.
  • Улучшить автодополнение в IDE (PhpStorm, VS Code).
  • Писать более самодокументируемый код.

Ограничения и подводные камни

При использовании эмуляции Generics важно помнить о следующих ограничениях:

  • Это не проверка во время выполнения — аннотации влияют только на статический анализ.
  • Нет синтаксической поддержки — код может быть менее читаемым из-за обилия комментариев

Блоги

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