Generics в PHP: зачем нужны и как использовать
Современная разработка на PHP требует не только скорости, но и надежности кода. Одним из инструментов, который помогает достичь этой цели, являются Generics (обобщенные типы). Несмотря на то, что PHP — язык с динамической типизацией, с выходом PHP 7.0 и выше, а особенно с PHP 8.0, статическая типизация стала доступна и востребована. Generics позволяют писать более гибкие, переиспользуемые и безопасные структуры данных, такие как коллекции, стеки, очереди и репозитории, без потери информации о типах.
В этой статье мы подробно разберем, что такое Generics в PHP, зачем они нужны, как их правильно использовать на практике, и рассмотрим конкретные примеры кода.
Что такое Generics и зачем они нужны?
Generics (обобщенные типы) — это механизм, позволяющий параметризовать классы, интерфейсы, трейты и функции типом данных. Вместо того чтобы писать отдельную реализацию для каждого типа (например, IntegerList, StringList, UserList), вы создаете один универсальный класс List<T>, где T — это placeholder (заменитель) для конкретного типа, который будет передан при использовании.
Зачем это нужно?
- Безопасность типов: IDE и статические анализаторы (Psalm, PHPStan) могут проверять, что в коллекцию попадают только элементы одного типа, предотвращая ошибки на этапе разработки.
- Уменьшение дублирования кода: Вам не нужно писать один и тот же класс для разных типов данных.
- Улучшение читаемости: Код становится самодокументируемым — сразу видно, какие типы данных ожидаются.
- Повышение производительности: Хотя PHP сам по себе не использует Generics на уровне виртуальной машины (как Java или C#), статические анализаторы помогают находить потенциальные ошибки до выполнения кода.
Как использовать Generics в PHP? Синтаксис и примеры
Начиная с PHP 8.0, была введена поддержка Generics на уровне аннотаций (docblock) и частично на уровне кода (для variadic arguments). Полноценная поддержка на уровне синтаксиса ожидается в будущих версиях, но уже сейчас можно эффективно использовать Generics с помощью PHPDoc и статических анализаторов.
1. Базовый синтаксис с PHPDoc
Самый распространенный способ — использовать аннотации @template и @param/@return с указанием обобщенного типа.
<?php
/**
* @template T
*/
class Collection {
/** @var array<T> */
private array $items;
/**
* @param T $item
*/
public function add($item): void {
$this->items[] = $item;
}
/**
* @return T|null
*/
public function get(int $index) {
return $this->items[$index] ?? null;
}
}
// Пример использования
$intCollection = new Collection();
$intCollection->add(42);
// $intCollection->add('string'); // Ошибка статического анализатора
$userCollection = new Collection();
$userCollection->add(new User());
// $userCollection->add(123); // Ошибка
?>
В этом примере @template T объявляет обобщенный тип. @param T $item и @return T|null говорят анализатору, что тип элементов должен быть согласован.
2. Ограничения типов (bounded generics)
Вы можете ограничить, какие типы могут использоваться в качестве параметра. Это делается с помощью @extends или @implements.
<?php
/**
* @template T of \Stringable
*/
class StringableCollection {
/** @var array<T> */
private array $items;
/**
* @param T $item
*/
public function add($item): void {
$this->items[] = $item;
}
}
// Можно использовать только классы, реализующие Stringable
class User implements \Stringable {
public function __toString(): string {
return 'User';
}
}
$collection = new StringableCollection();
$collection->add(new User());
// $collection->add(123); // Ошибка - int не реализует Stringable
?>
Здесь @template T of \Stringable означает, что T может быть только типом, который реализует интерфейс \Stringable.
3. Generics для функций и методов
Обобщенные типы работают не только с классами, но и с функциями.
<?php
/**
* @template T
* @param T $value
* @return T
*/
function identity($value) {
return $value;
}
$result = identity(42); // int
$result2 = identity('hello'); // string
?>
Типичные сценарии использования Generics в PHP
Generics особенно полезны в следующих ситуациях:
- Коллекции и контейнеры:
Collection<User>,Stack<int>,Map<string, Product>. Это самый частый случай. - Репозитории:
Repository<User>с методамиfind(int $id): ?Userиsave(User $user): void. - Паттерн "Строитель" (Builder):
Builder<Query>может возвращать конкретный тип запроса. - Прокси и декораторы:
Proxy<T>может оборачивать любой объект, сохраняя его тип. - Работа с API:
Response<UserData>гарантирует, что ответ будет содержать данные нужной структуры.
Пример: Репозиторий с Generics
<?php
/**
* @template T
*/
interface RepositoryInterface {
/**
* @param int $id
* @return T|null
*/
public function find(int $id);
}
/**
* @implements RepositoryInterface<User>
*/
class UserRepository implements RepositoryInterface {
public function find(int $id): ?User {
// Логика поиска
return new User();
}
}
// Теперь IDE знает, что find() возвращает User|null
$repo = new UserRepository();
$user =