skip to content

How would you use PHPStan's @template to type a collection class so that its elements keep their type through add(), get() and foreach?

level: seniorimportance: should knowfreq 35%

answer

  1. PHP has no native generics
  2. @template T above the class
  3. @param T, @return T on methods
  4. @implements IteratorAggregate<int, T>
  5. Collection<User>; invariant by default

basics

~20 s

Declare @template T above the class, use T in @param and @return tags, declare @implements IteratorAggregate<int, T> for foreach, and type usages as Collection<User>; PHPStan then infers User from get() and foreach and rejects adding other types.

solid answer

~40 s

PHP has no native generics, so PHPStan reads them from PHPDoc. Put `@template T` above the class, store items as `/** @var list<T> */`, annotate `add()` with `@param T $item` and `get()` with `@return T`. Because the class implements `IteratorAggregate`, add `@implements IteratorAggregate<int, T>` so `foreach` yields `T`; without it PHPStan reports that the class does not specify the interface's types. Callers write `@param Collection<User> $users`, or a static constructor can infer `T` from its arguments. A subclass either fixes the type, `@extends Collection<User>`, or stays generic by repeating `@template T` with `@extends Collection<T>`. `@template T of Entity` sets an upper bound. `@template` is invariant, so `Collection<Dog>` is not accepted where `Collection<Animal>` is expected; `@template-covariant` relaxes that for read-only collections.

code

php · 15 lines
php
<?php

declare(strict_types=1);

/**
 * @extends Collection<User>
 */
final class UserCollection extends Collection
{
    /** @return array<int, string> */
    public function emails(): array
    {
        return array_map(fn (User $u): string => $u->email, iterator_to_array($this));
    }
}

go deeper

for a junior

Know that PHPStan reads generics from PHPDoc: @template T on the class and T in @param and @return tags.

for a middle

Write a generic collection with list<T> storage, @implements IteratorAggregate<int, T> for foreach, and typed usages like Collection<User>.

for a senior

Handle subclasses with @extends, bounds with of, class-string<T> factories, invariance versus @template-covariant, and the level 6 missing generics errors.

for a principal

Decide where generic collections pay off versus plain typed arrays, and how the team keeps PHPDoc generics consistent and reviewed.

## The problem A hand-written collection class in PHP stores `mixed` as far as the engine is concerned: ```php final class Collection implements IteratorAggregate { private array $items = []; public function add(mixed $item): void { $this->items[] = $item; } public function get(int $i): mixed { return $this->items[$i]; } } ``` Every `get()` returns `mixed`, every `foreach` yields `mixed`, and at level 9 or 10 PHPStan will not let you call methods on those values. PHP has no native generics, but PHPStan supports them through **PHPDoc tags**. ## Making the class generic ```php <?php declare(strict_types=1); /** * @template T * @implements IteratorAggregate<int, T> */ class Collection implements IteratorAggregate { /** @var list<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]; } /** @return ArrayIterator<int, T> */ public function getIterator(): ArrayIterator { return new ArrayIterator($this->items); } } ``` What each tag does: - `@template T` declares a **type variable** for the class. The name is conventional; it must not collide with an existing class name. - `@param T` and `@return T` tie method signatures to that variable. - `@var list<T>` on the property keeps the internal storage typed. - `@implements IteratorAggregate<int, T>` tells PHPStan which key and value types the built-in generic interface has for this class, so `foreach ($users as $user)` yields `T`. `ArrayIterator` is generic through PHPStan's stubs, so `ArrayIterator<int, T>` checks too. ## Using it ```php /** @param Collection<User> $users */ function notify(Collection $users): void { foreach ($users as $user) { echo $user->email; // $user is User } $users->add(new Order()); // reported: User expected, Order given } ``` Callers state the type argument in angle brackets. Where a constructor or factory receives elements, PHPStan can **infer** `T` from the arguments instead. ## Subclasses, bounds and class-string | Need | Tag | |---|---| | a subclass fixed to one type | `@extends Collection<User>` above `UserCollection` | | a subclass that stays generic | `@template T` plus `@extends Collection<T>` | | implementing a generic interface | `@implements Repository<User>` | | using a generic trait | `@use HasItems<User>` on the `use` line | | restrict what T may be | `@template T of Entity` | | return an instance of a passed class name | `@param class-string<T> $class` with `@return T` | If you implement or extend a generic type without specifying its arguments, PHPStan reports, for example, *Class Bar implements generic interface Collection but does not specify its types: T*. That error carries the `missingType.generics` identifier and is part of level 6's missing-type checks. ## Variance By default `@template` is **invariant**: a function that takes `Collection<Animal>` does not accept `Collection<Dog>`, because it could add a `Cat` to it. For a read-only collection, `@template-covariant T` allows `Collection<Dog>` where `Collection<Animal>` is expected, at the price that `T` may no longer appear in parameter positions such as `add()`. Why invariance is the safe default is a general generics topic; in PHPStan the practical question is which tag to write. ## Generic functions and defaults `@template` also works on a single function or method, not only on classes: ```php /** * @template T * @param list<T> $items * @return T|null */ function firstOrNull(array $items): mixed { return $items[0] ?? null; } ``` PHPStan infers `T` at each call from the argument, so `firstOrNull($users)` returns `User|null`. Class-level type variables can also have **defaults**, for example `@template T = string`, so users may write `Pair` or `Pair<bool>` without spelling out every argument, and a default can be combined with a bound: `@template T of object = stdClass`. ## Compatibility with other tools If another tool in the chain does not understand `@template`, PHPStan also reads **prefixed tags**: `@phpstan-template`, `@phpstan-param`, `@phpstan-return`. You can keep simple unprefixed types for the other tool and precise generic types in the prefixed ones. ## Checklist for a reviewer 1. `@template` on the class, and every method signature that touches elements uses `T`. 2. Storage typed as `list<T>` or `array<K, T>`. 3. Built-in interfaces (`IteratorAggregate`, `ArrayAccess`, `Countable` where relevant) given their type arguments with `@implements`. 4. Usages typed as `Collection<Concrete>` rather than bare `Collection`. 5. Covariance chosen deliberately, not added to silence an error.

  • PHPStan reports 'implements generic interface IteratorAggregate but does not specify its types'; what do you add?
    An `@implements IteratorAggregate<int, T>` tag above the class (or a concrete type such as `<int, User>`). It tells PHPStan the key and value types the built-in generic interface has for this class, so foreach over it yields the element type.
  • Why does a function accepting Collection<Animal> reject Collection<Dog>, and what can change that?
    `@template` is invariant by default, because the function could add a Cat to what is really a collection of dogs. If the collection is read-only, declaring `@template-covariant T` lets `Collection<Dog>` be passed, but T can then no longer be used in parameter positions such as `add()`.

saying these in an interview costs you the question

  • PHP 8.5 supports native generics like Collection<User>
  • @template changes runtime type checks in PHP
  • Collection<Dog> is always accepted where Collection<Animal> is expected
  • Implementing IteratorAggregate needs no type arguments for PHPStan
  • A subclass inherits T automatically without @extends