Введение: что такое Generics и почему они важны в PHP
Современная разработка на PHP всё чаще требует строгой типизации и переиспользуемого кода. Одним из ключевых инструментов, позволяющих достичь этих целей, являются Generics (обобщения или дженерики). В PHP поддержка Generics была официально добавлена начиная с версии PHP 8.0, хотя ранее существовали различные утилиты и аннотации (например, через PHPDoc).
В этой статье мы разберём, зачем нужны Generics в PHP, как они работают на практике, и приведём конкретные примеры кода, которые помогут вам писать более безопасный, читаемый и масштабируемый код.
Зачем нужны Generics в PHP?
Основная цель Generics — обеспечить безопасность типов во время выполнения, не теряя гибкости обобщённого кода. Представьте, что вы пишете функцию для работы с коллекцией элементов. Без Generics вы либо используете смешанные типы (mixed), либо пишете отдельные функции для каждого типа данных. Оба подхода ведут к ошибкам и дублированию кода.
Вот основные преимущества использования Generics:
- Типобезопасность — ошибки типов выявляются на этапе компиляции, а не в рантайме.
- Переиспользование кода — одна функция или класс работают с любыми типами данных.
- Читаемость — код становится самодокументируемым: сразу видно, какие типы принимаются и возвращаются.
- Интеграция с IDE — современные редакторы (PhpStorm, VS Code) лучше понимают код с Generics, предлагая автодополнение и подсказки.
Как работают Generics в PHP: синтаксис и основные конструкции
В PHP Generics реализованы через шаблоны типов, которые задаются в угловых скобках <T>. Буква T — это условное обозначение типа, который будет подставлен при вызове функции или создании объекта. Вы можете использовать любые имена: T, K, V или осмысленные названия.
1. Обобщённые функции
Рассмотрим простейший пример — функцию, которая возвращает первый элемент массива:
<?php
function firstElement(array $items): mixed {
return $items[0] ?? null;
}
// Использование
$numbers = [1, 2, 3];
$first = firstElement($numbers); // тип mixed, IDE не знает, что это int
С Generics мы можем указать, что функция работает с массивом элементов любого типа, но возвращает именно тот тип, который был передан:
<?php
/**
* @template T
* @param array<T> $items
* @return T|null
*/
function firstElement(array $items): mixed {
return $items[0] ?? null;
}
// Теперь IDE понимает, что $first — int|null
$first = firstElement([1, 2, 3]);
Обратите внимание: в PHP 8.0+ Generics поддерживаются через PHPDoc-аннотации. Нативная поддержка (без комментариев) пока отсутствует, но это стандарт де-факто для современных проектов.
2. Обобщённые классы
Самый частый сценарий использования Generics — это классы-коллекции. Создадим простой класс Collection, который может хранить элементы любого типа:
<?php
/**
* @template T
*/
class Collection {
/** @var array<T> */
private array $items = [];
/**
* @param T $item
* @return void
*/
public function add(mixed $item): void {
$this->items[] = $item;
}
/**
* @return T|null
*/
public function get(int $index): mixed {
return $this->items[$index] ?? null;
}
/**
* @return array<T>
*/
public function all(): array {
return $this->items;
}
}
// Использование
$intCollection = new Collection<int>();
$intCollection->add(10);
$intCollection->add(20);
// $intCollection->add('string'); // Ошибка типа, если IDE проверяет
$stringCollection = new Collection<string>();
$stringCollection->add('Hello');
$stringCollection->add('World');
Теперь, если вы попытаетесь добавить строку в коллекцию целых чисел, среда разработки (IDE) укажет на ошибку до запуска кода.
3. Ограничения типов (Bounds)
Иногда нужно ограничить допустимые типы. Например, мы хотим, чтобы коллекция работала только с объектами, реализующими интерфейс Comparable:
<?php
interface Comparable {
public function compareTo(object $other): int;
}
/**
* @template T of Comparable
*/
class SortedCollection {
/** @var array<T> */
private array $items = [];
/**
* @param T $item
*/
public function add(Comparable $item): void {
$this->items[] = $item;
usort($this->items, function (Comparable $a, Comparable $b) {
return $a->compareTo($b);
});
}
}
Здесь @template T of Comparable означает, что T должен быть подтипом Comparable. Это гарантирует, что в коллекцию попадут только объекты с нужным методом.
Практические примеры использования Generics
1. Репозитории и Data Mapper
В архитектуре приложений часто используются паттерны Repository и Data Mapper. Generics позволяют создать единый интерфейс для работы с разными сущностями:
<?php
/**
* @template T
*/
interface Repository {
/**
* @param int $id
* @return T|null
*/
public function find(int $id): mixed;
/**
* @return array<T>
*/
public function findAll(): array;
/**
* @param T $entity
*/
public function save(mixed $entity): void;
}
/**
* @implements Repository<User>
*/
class UserRepository implements Repository {
public function find(int $id): mixed {
// логика поиска пользователя
}
public function findAll(): array {
// логика получения всех пользователей
}
public function save(mixed $entity): void {
// логика сохранения
}
}
2. Сервисы с Dependency Injection
Generics отлично работают с контейнерами внедрения зависимостей. Например, можно создать универсальный серви