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 важно помнить о следующих ограничениях:
- Это не проверка во время выполнения — аннотации влияют только на статический анализ.
- Нет синтаксической поддержки — код может быть менее читаемым из-за обилия комментариев