Why Generics Are Not Just a Trend, but a Necessity?
\n\nIf you've ever written PHP code that worked with collections, repositories, or a service layer, you've likely encountered a situation: a function accepts array, but what's inside is a mystery. Maybe it's users, or maybe it's cats. Or, even worse — both. This is exactly where Generics come to the rescue — a powerful typing tool that makes your code predictable, safe, and self-documenting.
In this article, we'll break down how generics work in PHP, why they are needed, and how to use them in real projects. You'll learn how to avoid common mistakes and make your code professional. Let's go!
\n\nWhat Are Generics in Simple Terms?
\n\nImagine you are an online store developer. You have a Cart class that stores items. Without generics, you write:
class Cart {\n private array $items;\n \n public function addItem($item): void {\n $this->items[] = $item;\n }\n \n public function getItems(): array {\n return $this->items;\n }\n}\n\nThe problem? $item can be anything — a number, a string, a Product object, or even null. The IDE won't tell you what type is returned from getItems(). You'll have to dig into the documentation or debug.
Generics solve this problem. They allow you to specify: "Hey, this class only works with products!". It looks like this:
\n\nclass Cart {\n /** @param Product[] $items */\n private array $items;\n \n public function addItem(Product $item): void {\n $this->items[] = $item;\n }\n \n /** @return Product[] */\n public function getItems(): array {\n return $this->items;\n }\n}\n\nBut this is just an imitation. Real generics in PHP appeared with PHP 8.0, and full support came in PHP 8.1+ using attributes and static analysis (e.g., PHPStan or Psalm).
\n\nHow Do Generics Actually Work in PHP?
\n\nUnlike Java or C#, PHP does not have native syntax-level support for generics (yet). However, thanks to PHPDoc annotations and static analysis tools, we can get all the benefits of typing.
\n\nTemplate Types
\n\nThe most popular approach is to use @template in PHPDoc. Let's look at an example with a collection:
/**\n * @template T\n */\nclass Collection {\n /** @var array<int, T> */\n private array $items = [];\n \n /**\n * @param T $item\n */\n public function add(mixed $item): void {\n $this->items[] = $item;\n }\n \n /**\n * @return T\n */\n public function get(int $index): mixed {\n return $this->items[$index] ?? throw new \\OutOfBoundsException();\n }\n}\n\nNow, when using this class, you specify the concrete type:
\n\n/** @var Collection<User> $userCollection */\n$userCollection = new Collection();\n$userCollection->add(new User('Alice'));\n$user = $userCollection->get(0); // PHPStan knows: $user is a User\necho $user->name; // Safe!\n\nConstraints for Generics
\n\nSometimes you need the type T to not be just anything, but to implement a specific interface. This is called a bound:
\n\n/**\n * @template T of \\JsonSerializable\n */\nclass JsonCollection {\n /** @var array<int, T> */\n private array $items = [];\n \n public function toJson(): string {\n return json_encode($this->items);\n }\n}\n\nNow you won't be able to add an object to the collection that does not implement JsonSerializable. The IDE and static analyzer will throw an error.