Generics with @template
A collection of objects works perfectly at runtime. Then you write $products->first()->name, and PHPStan says object has no property $name. You could write a ProductCollection, then an OrderCollection, then a CustomerCollection, all the same apart from the type. Or you could declare the type once and let it vary.
@template
@template T gives a class a type parameter. Use T anywhere in its docblocks that the item type goes:
/** * @template T */ final class Queue { /** @param list<T> $items */ public function __construct(private array $items) {} /** @return T|null */ public function next(): mixed { return array_shift($this->items); } }
Now Queue<Email> is a type you can put on a parameter or property, and PHPStan knows next() gives you an Email or null. You rarely have to write it when you create one, because PHPStan works out T from the constructor: new Queue([$welcome, $receipt]) is a Queue<Email>.
PHP has no native generics, so the native return type stays mixed and the docblock says T.
Methods can have their own
Some methods return a different type from the one the class holds. Give the method its own template, and PHPStan works it out from the arguments at each call:
/** * @template T * @param callable(): T $compute * @return T */ public function remember(string $key, callable $compute): mixed
$cache->remember('stock', fn (): int => $this->countStock()) returns an int, with no @var needed. A collection's map() works the same way: it takes Ts and returns a collection of U, whatever the callback returns.
Generic types you already use
IteratorAggregate, Traversable, ArrayIterator and Generator are all generic. From level 6, PHPStan asks what they contain, just as it asks about arrays: @implements IteratorAggregate<int, T>, @return Generator<int, Product>.
Power-up
<?php final class Cache { public function remember(string $key, callable $compute): mixed { return $this->items[$key] ??= $compute(); } } /** @var int $stock every caller has to say what came back */ $stock = $cache->remember('stock', fn () => $this->countStock());
<?php final class Cache { /** * @template T * @param callable(): T $compute * @return T */ public function remember(string $key, callable $compute): mixed { return $this->items[$key] ??= $compute(); } } // PHPStan knows $stock is an int, from the callback. $stock = $cache->remember('stock', fn (): int => $this->countStock());