Generics в PHP: полный гайд по типизации с примерами

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

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

Начать курс

Почему Generics — это не просто модный тренд, а необходимость?

Если вы когда-нибудь писали на PHP код, который работал с коллекциями, репозиториями или сервисным слоем, вы наверняка сталкивались с ситуацией: функция принимает array, а что внутри — загадка. Может, там пользователи, а может, котики. Или, что ещё хуже, — и то, и другое. Именно здесь на помощь приходят Generics (дженерики) — мощный инструмент типизации, который делает ваш код предсказуемым, безопасным и самодокументируемым.

В этой статье мы разберём, как работают generics в PHP, зачем они нужны и как их использовать на реальных проектах. Вы узнаете, как избежать типичных ошибок и сделать ваш код профессиональным. Поехали!

Что такое Generics простыми словами?

Представьте, что вы — разработчик интернет-магазина. У вас есть класс Cart, который хранит товары. Без generics вы пишете:

class Cart {
    private array $items;
    
    public function addItem($item): void {
        $this->items[] = $item;
    }
    
    public function getItems(): array {
        return $this->items;
    }
}

Проблема? $item может быть чем угодно — числом, строкой, объектом Product или даже null. IDE не подскажет, какой тип возвращается из getItems(). Придётся лезть в документацию или дебажить.

Generics решают эту проблему. Они позволяют указать: «Эй, этот класс работает только с товарами!». Выглядит это так:

class Cart {
    /** @param Product[] $items */
    private array $items;
    
    public function addItem(Product $item): void {
        $this->items[] = $item;
    }
    
    /** @return Product[] */
    public function getItems(): array {
        return $this->items;
    }
}

Но это лишь имитация. Настоящие generics в PHP появились с PHP 8.0, а полноценная поддержка — в PHP 8.1+ с помощью атрибутов и статического анализа (например, PHPStan или Psalm).

Как работают Generics в PHP на самом деле?

В отличие от Java или C#, PHP не имеет нативной поддержки generics на уровне синтаксиса (пока). Однако благодаря аннотациям PHPDoc и инструментам статического анализа мы можем получить все преимущества типизации.

Шаблонные типы (Template Types)

Самый популярный подход — использовать @template в PHPDoc. Рассмотрим пример с коллекцией:

/**
 * @template T
 */
class Collection {
    /** @var array<int, T> */
    private array $items = [];
    
    /**
     * @param T $item
     */
    public function add(mixed $item): void {
        $this->items[] = $item;
    }
    
    /**
     * @return T
     */
    public function get(int $index): mixed {
        return $this->items[$index] ?? throw new \OutOfBoundsException();
    }
}

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

/** @var Collection<User> $userCollection */
$userCollection = new Collection();
$userCollection->add(new User('Alice'));
$user = $userCollection->get(0); // PHPStan знает: $user — это User
echo $user->name; // Безопасно!

Ограничения (Constraints) для Generics

Иногда нужно, чтобы тип T не был любым, а реализовывал определённый интерфейс. Это называется bound (ограничение):

/**
 * @template T of \JsonSerializable
 */
class JsonCollection {
    /** @var array<int, T> */
    private array $items = [];
    
    public function toJson(): string {
        return json_encode($this->items);
    }
}

Теперь вы не сможете добавить в коллекцию объект, который не реализует JsonSerializable. IDE и статический анализатор выдадут ошибку.

Практические примеры: от простого к сложному

Пример 1: Generic-репозиторий

Допустим, вы пишете приложение с несколькими сущностями: User, Product, Order. Вместо того чтобы писать отдельный репозиторий для каждой, используем generics:

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

/**
 * @template T of object
 * @implements RepositoryInterface<T>
 */
class BaseRepository implements RepositoryInterface {
    /** @param class-string<T> $entityClass */
    public function __construct(
        private string $entityClass
    ) {}
    
    public function find(int $id): ?object {
        // Логика поиска
        $data = $this->fetchFromDB($id);
        return $data ? new ($this->entityClass)(...$data) : null;
    }
    
    public function save(object $entity): void {
        // Сохраняем в БД
    }
}

// Использование:
/** @var BaseRepository<User> $userRepo */
$userRepo = new BaseRepository(User::class);
$user = $userRepo->find(42); // PHPStan знает: $user — User|null

Пример 2: Generic-сервис с кэшированием

Представьте сервис, который получает данные из API и кэширует их:

/**
 * @template T
 */
class CachedService {
    /** @param callable(): T $fetcher */
    public function __construct(
        private mixed $fetcher,
        private int $ttl = 3600
    ) {}
    
    /**
     * @return T
     */
    public function get(): mixed {
        $cacheKey = spl_object_hash($this->fetcher);
        if ($cached = $this->getFromCache($cacheKey)) {
            return $cached;
        }
        
        $result = ($this->fetcher)();
        $this->saveToCache($cacheKey, $result);
        return $result;
    }
}

// Использование:
$weatherService = new CachedService(
    fn() => $api->getWeather('Moscow')
);
$weather = $weatherService->get(); // Тип — array|null (зависит от API)

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

В PHP 8.1+ появилась поддержка ковариантности (когда дочерний тип может быть более специфичным) и контравариантности (когда параметры могут быть менее специфичными). Это важно для generics.

Пример ковариантности с коллекциями:

class Animal {}
class Dog extends Animal {}

/** @template T */
class AnimalShelter {
    /** @param T $animal */
    public function __construct(
        protected mixed $animal
    ) {}
}

/** @extends AnimalShelter<Dog> */
class DogShelter extends AnimalShelter {
    public function __construct(Dog $dog) {
        parent::__construct($dog);
    }
}

Теперь DogShelter можно использовать там, где ожидается AnimalShelter<Animal> — благодаря ковариантности.

Частые ошибки и как их избежать

  • Забыть указать @template — без этого generics не работают. Всегда начинайте с объявления шаблонного типа

Блоги

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