Generics via @template Docblocks
PHP has no runtime generics yet, but @template docblocks give tools like PHPStan and Psalm full generic type-checking today.
Real generics aren't in the PHP engine (yet), but you don't have to give up type safety on collections and containers. Static analysers understand @template annotations — you write a docblock that declares a type variable, and the tool tracks it through your code, catching mismatches before runtime.
Easy example
<?php
/**
* @template T
*/
class Box {
/** @param T $value */
public function __construct(private mixed $value) {}
/** @return T */
public function get(): mixed {
return $this->value;
}
}
/** @var Box<string> $b */
$b = new Box('hello');
echo $b->get();The engine still sees mixed, so nothing changes at runtime — but your editor and CI now know $b->get() is a string.
Example
When to use it
- Annotate a generic TypedCollection<T> with @template T so PHPStan knows the collection's element type at each call site.
- Use @param T and @return T on a generic identity function to preserve the type through analysis.
- Document a repository's findById with @return T where T is constrained to Model subclasses for IDE auto-complete.
More examples
@template on a typed collection
@template T declares a type parameter; @var TypedList<string> tells PHPStan this instance holds strings.
<?php
/**
* @template T
*/
class TypedList {
/** @var T[] */
private array $items = [];
/** @param T $item */
public function add(mixed $item): void {
$this->items[] = $item;
}
/** @return T[] */
public function all(): array {
return $this->items;
}
}
/** @var TypedList<string> $names */
$names = new TypedList();
$names->add('Alice');
$names->add('Bob');
print_r($names->all());@return static for fluent builders
@return static tells static analysis that the return type matches the called class, preserving MailBuilder through the chain.
<?php
class Builder {
protected array $options = [];
/**
* @return static
*/
public function set(string $key, mixed $value): static {
$clone = clone $this;
$clone->options[$key] = $value;
return $clone;
}
}
class MailBuilder extends Builder {
public function subject(string $s): static {
return $this->set('subject', $s);
}
}
$mail = (new MailBuilder())->subject('Hello')->set('from', '[email protected]');
var_dump($mail->options);@template with class constraint
@template T of Throwable constrains T to exception types; class-string<T> lets PHPStan verify the passed class is correct.
<?php
/**
* @template T of \Throwable
* @param class-string<T> $exceptionClass
* @param callable(): mixed $callback
* @return mixed
*/
function tryCatch(string $exceptionClass, callable $callback): mixed {
try {
return $callback();
} catch (\Throwable $e) {
if ($e instanceof $exceptionClass) {
return null;
}
throw $e;
}
}
$result = tryCatch(\RuntimeException::class, fn() => throw new \RuntimeException('oops'));
var_dump($result); // NULL
Discussion