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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.