Phan v6 significantly expands support for generic (templated) classes, interfaces, traits, and functions using PHPDoc annotations with @template and related tags. This allows you to write type-safe, reusable code that works with multiple types while maintaining strong static analysis.
What's New in v6:
- Generic interfaces with
@implements/@phan-implements - Generic traits with
@use/@phan-use - Template constraints with
@template T of SomeClass - Variance annotations:
@template-covariantand@template-contravariant - Utility types:
key-of<T>,value-of<T>,int-range<min, max>,positive-int,negative-int
- Basic Generic Classes
- Generic Interfaces
- Generic Traits
- Template Constraints
- Variance Annotations
- Function Templates
- Utility Types
- Rules and Limitations
All template types for a generic class must be declared on the class via doc block comments using the @template annotation.
/**
* A generic box that holds a value of type T
* @template T
*/
class Box {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
/** @return T */
public function get() {
return $this->value;
}
}
// Phan infers Box<int> from the constructor argument
$intBox = new Box(42);
$val = $intBox->get(); // Phan knows this is an int
// Phan infers Box<string> from the constructor argument
$strBox = new Box("hello");
$str = $strBox->get(); // Phan knows this is a stringTry this example in Phan-in-Browser →
All template types must be filled in with concrete types via the constructor. This allows Phan to infer the generic type from constructor arguments.
/**
* @template T
*/
class Container {
/** @var T */
protected $item;
/** @param T $item */
public function __construct($item) {
$this->item = $item;
}
}Try this example in Phan-in-Browser →
If template types aren't specified as constructor parameters, Phan will emit PhanGenericConstructorTypes.
Classes extending generic classes need to fill in the types of the generic parent class via the @extends annotation.
/**
* @template T
*/
class Box {
/** @var T */
protected $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
}
// Concrete type parameter
/**
* @extends Box<int>
*/
class IntBox extends Box {
public function __construct(int $value) {
parent::__construct($value);
}
}
// Passing template through
/**
* @template T2
* @extends Box<T2>
*/
class GenericBox extends Box {
/** @param T2 $value */
public function __construct($value) {
parent::__construct($value);
}
}Try this example in Phan-in-Browser →
New in v6: Phan now supports generic interfaces using the @implements annotation.
/**
* @template T
*/
interface Repository {
/**
* @param int $id
* @return T
*/
public function find(int $id);
/**
* @param T $entity
*/
public function save($entity): void;
}Try this example in Phan-in-Browser →
Use the @implements annotation to specify the template type when implementing a generic interface:
class User {
public string $name = '';
}
/**
* @implements Repository<User>
*/
class UserRepository implements Repository {
public function find(int $id): User {
return new User();
}
public function save($entity): void {
// Phan knows $entity is User
echo $entity->name;
}
}
$repo = new UserRepository();
$user = $repo->find(1);
echo $user->name; // Phan knows $user is UserTry this example in Phan-in-Browser →
You can implement multiple generic interfaces with different template parameters:
/**
* @template TKey
* @template TValue
* @implements Iterator<TKey, TValue>
* @implements ArrayAccess<TKey, TValue>
*/
class Collection implements Iterator, ArrayAccess {
/** @var array<TKey, TValue> */
private $items = [];
// Iterator implementation...
public function current(): mixed { return current($this->items); }
public function key(): mixed { return key($this->items); }
public function next(): void { next($this->items); }
public function rewind(): void { reset($this->items); }
public function valid(): bool { return key($this->items) !== null; }
// ArrayAccess implementation...
public function offsetExists(mixed $offset): bool {
return isset($this->items[$offset]);
}
public function offsetGet(mixed $offset): mixed {
return $this->items[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void {
$this->items[$offset] = $value;
}
public function offsetUnset(mixed $offset): void {
unset($this->items[$offset]);
}
}Try this example in Phan-in-Browser →
Template parameters can themselves be generic:
/**
* @template T
*/
interface Repository {
/** @return T */
public function find(int $id);
}
/**
* @implements Repository<array<string, User>>
*/
class UserMapRepository implements Repository {
/**
* @return array<string, User>
*/
public function find(int $id): array {
return ['user' => new User()];
}
}Try this example in Phan-in-Browser →
You can use @phan-implements if you want to use Phan-specific annotations that don't interfere with other tools:
/**
* @template T
* @phan-implements Repository<T>
*/
class GenericRepository implements Repository {
// Implementation...
}Try this example in Phan-in-Browser →
New in v6: Phan now supports generic traits using the @use annotation.
/**
* @template T
*/
trait Repository {
/** @var list<T> */
private $items = [];
/**
* @param T $item
*/
public function add($item): void {
$this->items[] = $item;
}
/**
* @return T|null
*/
public function first() {
return $this->items[0] ?? null;
}
}Try this example in Phan-in-Browser →
Use the @use annotation to specify the template type when using a generic trait:
class Article {
public string $title = '';
}
/**
* @use Repository<Article>
*/
class ArticleService {
use Repository;
}
$service = new ArticleService();
$service->add(new Article());
$article = $service->first(); // Phan knows this is Article|null
echo $article->title;Try this example in Phan-in-Browser →
/**
* @template T
*/
trait Storage {
/** @var T */
private $data;
/** @param T $value */
public function store($value): void {
$this->data = $value;
}
/** @return T */
public function retrieve() {
return $this->data;
}
}
/**
* @template T
* @use Storage<T>
*/
class Container {
use Storage;
/** @param T $initial */
public function __construct($initial) {
$this->store($initial);
}
}
$container = new Container(42);
$value = $container->retrieve(); // Phan knows this is an intTry this example in Phan-in-Browser →
Similar to @phan-implements, you can use @phan-use for Phan-specific annotations:
/**
* @template T
* @phan-use Repository<T>
*/
class Service {
use Repository;
}Try this example in Phan-in-Browser →
New in v6: Template parameters can be constrained to specific types using @template T of SomeClass.
Constraints ensure that template arguments satisfy a specific type bound:
class Animal {
public function makeSound(): string {
return "some sound";
}
}
class Dog extends Animal {
public function makeSound(): string {
return "woof";
}
}
class Cat extends Animal {
public function makeSound(): string {
return "meow";
}
}
/**
* @template T of Animal
*/
class Shelter {
/** @var T */
private $resident;
/** @param T $animal */
public function __construct($animal) {
$this->resident = $animal;
}
/** @return T */
public function getResident() {
return $this->resident;
}
public function hearSound(): string {
// Phan knows $resident has makeSound() method
return $this->resident->makeSound();
}
}
// OK: Dog extends Animal
/**
* @extends Shelter<Dog>
*/
class DogShelter extends Shelter {}
// ERROR: string does not extend Animal
/**
* @extends Shelter<string>
*/
class InvalidShelter extends Shelter {}Try this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected error on InvalidShelter:
# PhanTemplateTypeConstraintViolation Template type T of \Shelter must be compatible with \Animal, but string was providedinterface Timestamped {
public function getTimestamp(): int;
}
/**
* @template T of Timestamped
*/
class TimestampedCollection {
/** @var list<T> */
private $items = [];
/** @param T $item */
public function add($item): void {
$this->items[] = $item;
}
/**
* @return T|null
*/
public function getMostRecent() {
$latest = null;
foreach ($this->items as $item) {
if ($latest === null || $item->getTimestamp() > $latest->getTimestamp()) {
$latest = $item;
}
}
return $latest;
}
}Try this example in Phan-in-Browser →
Template constraints work on functions and methods too:
/**
* @template T of Animal
* @param T $animal
* @return T
*/
function cloneAnimal($animal) {
return clone $animal;
}
$dog = new Dog();
$clonedDog = cloneAnimal($dog); // Phan knows this is Dog
// ERROR: string is not an Animal
$str = cloneAnimal("not an animal");Try this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected error: PhanTemplateTypeConstraintViolationConstraints can be union types:
/**
* @template T of int|string
* @param T $value
* @return T
*/
function identity($value) {
return $value;
}
identity(42); // OK
identity("test"); // OK
identity(true); // ERROR: bool is not int|stringTry this example in Phan-in-Browser →
New in v6: Constraints can require multiple interfaces:
interface Serializable {
public function serialize(): string;
}
interface Validatable {
public function validate(): bool;
}
/**
* @template T of Serializable&Validatable
*/
class Processor {
/** @param T $item */
public function process($item): string {
if ($item->validate()) {
return $item->serialize();
}
return '';
}
}Try this example in Phan-in-Browser →
New in v6: Phan supports variance annotations to enforce proper template usage in read/write positions.
Use @template-covariant T when the template type is only used in "output" positions (return types, readonly properties):
/**
* @template-covariant T
*/
interface Producer {
/** @return T */
public function produce();
}
/**
* @template-covariant T
*/
class ReadOnlyBox {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
/** @return T */
public function get() {
return $this->value;
}
// ERROR: covariant template in parameter position
/** @param T $value */
public function set($value): void {
$this->value = $value;
}
}Try this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected error: PhanTemplateTypeVarianceViolationCovariant templates enable safe subtyping:
// With Producer<T> being covariant:
// Producer<Dog> is a subtype of Producer<Animal>
// This is safe because you can only read from itTry this example in Phan-in-Browser →
Use @template-contravariant T when the template type is only used in "input" positions (parameters):
/**
* @template-contravariant T
*/
interface Consumer {
/** @param T $value */
public function consume($value): void;
}
/**
* @template-contravariant T
*/
class Sink {
/** @param T $value */
public function accept($value): void {
// Process value
}
// ERROR: contravariant template in return position
/** @return T */
public function produce() {
throw new Exception("Cannot produce");
}
}Try this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected error: PhanTemplateTypeVarianceViolationContravariant templates enable safe subtyping in the opposite direction:
// With Consumer<T> being contravariant:
// Consumer<Animal> is a subtype of Consumer<Dog>
// This is safe because you can only write to itTry this example in Phan-in-Browser →
Without variance annotations, templates are invariant and can be used in both positions:
/**
* @template T (invariant by default)
*/
class Box {
/** @var T */
private $value;
/** @param T $value */
public function set($value): void { // OK: can write
$this->value = $value;
}
/** @return T */
public function get() { // OK: can read
return $this->value;
}
}Try this example in Phan-in-Browser →
New in v6: Phan enforces variance rules on properties:
- Covariant templates are only allowed on
readonlyor@phan-read-onlyproperties - Contravariant templates are not allowed on any properties
- Arrays and other mutable structures require invariant templates
/**
* @template-covariant T
*/
class Container {
/**
* OK: readonly property with covariant template
* @var T
*/
public readonly $value;
/**
* ERROR: mutable property with covariant template
* @var T
*/
public $mutableValue;
/**
* ERROR: even readonly arrays are invariant
* @var array<T>
* @readonly
*/
public $items;
}Try this example in Phan-in-Browser →
Templates can be inferred from function and method parameters, useful for inferring return types.
/**
* @template T
* @param T[] $array
* @return T
* @throws InvalidArgumentException
*/
function first(array $array) {
if (count($array) === 0) {
throw new InvalidArgumentException("Array is empty");
}
return reset($array);
}
$users = [new User(), new User()];
$user = first($users); // Phan knows this is User
$numbers = [1, 2, 3];
$num = first($numbers); // Phan knows this is an intTry this example in Phan-in-Browser →
/**
* @template T
* @param Closure(int):T $callback
* @return list<T>
*/
function generate(Closure $callback, int $count): array {
$result = [];
for ($i = 0; $i < $count; $i++) {
$result[] = $callback($i);
}
return $result;
}
$strings = generate(
function(int $i): string {
return "Item $i";
},
5
);
// Phan infers list<string>
$objects = generate(
function(int $i): stdClass {
$obj = new stdClass();
$obj->index = $i;
return $obj;
},
3
);
// Phan infers list<stdClass>Try this example in Phan-in-Browser →
/**
* @template TKey
* @template TValue
* @param iterable<TKey, TValue> $items
* @return array<TKey, TValue>
*/
function toArray(iterable $items): array {
$result = [];
foreach ($items as $key => $value) {
$result[$key] = $value;
}
return $result;
}Try this example in Phan-in-Browser →
Create instances of classes based on their name:
/**
* @template T
* @param class-string<T> $className
* @return T
*/
function create(string $className) {
return new $className();
}
$user = create(User::class); // Phan knows this is User
$article = create(Article::class); // Phan knows this is ArticleTry this example in Phan-in-Browser →
New in v6: Phan supports several utility types for more precise type definitions.
Extract the key type from an array shape or generic array:
/**
* @param key-of<array{foo: int, bar: string, baz: bool}> $key
*/
function processKey(string $key): void {
// $key can only be 'foo', 'bar', or 'baz'
echo $key;
}
processKey('foo'); // OK
processKey('bar'); // OK
processKey('qux'); // ERROR: 'qux' is not a valid keyTry this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected error: PhanTypeMismatchArgumentProbablyRealWith generic arrays:
/**
* @template T
* @param array<string, T> $array
* @param key-of<array<string, T>> $key
* @return T
*/
function getByKey(array $array, $key) {
return $array[$key];
}Try this example in Phan-in-Browser →
Extract the value type from an array shape or generic array:
/**
* @param value-of<array{age: int, name: string}> $value
*/
function processValue($value): void {
// $value can be int or string
}
processValue(42); // OK
processValue("John"); // OK
processValue(true); // ERROR: bool is not int|stringTry this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected error: PhanTypeMismatchArgumentDefine an integer range with inclusive bounds:
/**
* @param int-range<1, 100> $percentage
*/
function setOpacity(int $percentage): void {
echo "Opacity: $percentage%";
}
setOpacity(50); // OK
setOpacity(100); // OK
setOpacity(0); // ERROR: 0 is below minimum
setOpacity(150); // ERROR: 150 exceeds maximumTry this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected errors: PhanTypeMismatchArgument for out-of-range valuesRanges work with literal values:
/**
* @param int-range<-10, 10> $offset
*/
function adjustPosition(int $offset): void {
// $offset is between -10 and 10
}
adjustPosition(-5); // OK
adjustPosition(11); // ERRORTry this example in Phan-in-Browser →
Shortcuts for common integer ranges:
/**
* @param positive-int $count
*/
function createItems(int $count): array {
// $count must be > 0
return array_fill(0, $count, null);
}
createItems(5); // OK
createItems(0); // ERROR: 0 is not positive
createItems(-1); // ERROR: -1 is not positive
/**
* @param negative-int $debt
*/
function recordDebt(int $debt): void {
// $debt must be < 0
echo "Debt: $debt";
}
recordDebt(-100); // OK
recordDebt(0); // ERROR: 0 is not negative
recordDebt(50); // ERROR: 50 is not negativeTry this example in Phan-in-Browser →
Test this example:
~/phan/phan -n example.php
# Expected errors for invalid valuesAll template types of a generic class must be inferable from the constructor parameters:
/**
* @template T
*/
class Box {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) { // T is inferred from $value
$this->value = $value;
}
}
// OK: Phan can infer T from constructor argument
$box = new Box(42); // Inferred as Box<int>Try this example in Phan-in-Browser →
If a template cannot be inferred, Phan will emit PhanGenericConstructorTypes.
Constants and static methods on a generic class cannot reference template types:
/**
* @template T
*/
class Container {
// ERROR: static property cannot use template type T
/** @var T */
private static $default;
// ERROR: static method cannot use template type T
/** @return T */
public static function getDefault() {
return self::$default;
}
}Try this example in Phan-in-Browser →
Workaround: Use function-level templates on static methods:
/**
* @template T
*/
class Container {
/**
* @template U
* @param U $value
* @return U
*/
public static function identity($value) {
return $value;
}
}Try this example in Phan-in-Browser →
When using @extends, @implements, or @use, the number of template parameters must match:
/**
* @template T
* @template U
*/
interface Pair {
// ...
}
// ERROR: Missing template parameter
/**
* @implements Pair<int>
*/
class BadPair implements Pair {}
// ERROR: Too many template parameters
/**
* @implements Pair<int, string, bool>
*/
class BadPair2 implements Pair {}
// OK: Correct number of parameters
/**
* @implements Pair<int, string>
*/
class GoodPair implements Pair {}Try this example in Phan-in-Browser →
Phan will emit warnings for mismatched parameter counts.
Child class templates do not automatically inherit bounds from parent templates:
/**
* @template T of Animal
*/
class AnimalContainer {}
/**
* @template T // This T is NOT constrained to Animal
*/
class SpecificContainer extends AnimalContainer {}Try this example in Phan-in-Browser →
Workaround: Explicitly redeclare the constraint:
/**
* @template T of Animal
*/
class SpecificContainer extends AnimalContainer {}Try this example in Phan-in-Browser →
Arrays and other mutable nested structures are always treated as invariant, even with variance annotations:
/**
* @template-covariant T
*/
class Container {
/**
* ERROR: arrays are invariant, even with covariant template
* @var array<T>
*/
public $items;
}Try this example in Phan-in-Browser →
This is because arrays are mutable in both directions (read and write).
Phan's generics implementation is designed to minimize false positives while maintaining analysis speed:
- Template validation is lazy (only when instantiated)
- Constraint checks are cached
- No additional analysis passes required
For best performance:
- Use constraints judiciously (only when needed for type safety)
- Avoid deeply nested generic types when simpler types suffice
- Prefer concrete types over templates in hot code paths when type safety isn't critical
Here's a complete example showing many generic features:
<?php
/**
* Generic collection with type constraints
*
* @template T of object
*/
class Collection {
/** @var list<T> */
private $items;
/**
* @param list<T> $items
*/
public function __construct(array $items = []) {
$this->items = $items;
}
/**
* Add an item to the collection
* @param T $item
*/
public function add($item): void {
$this->items[] = $item;
}
/**
* Get all items
* @return list<T>
*/
public function all(): array {
return $this->items;
}
/**
* Get first item
* @return T|null
*/
public function first() {
return $this->items[0] ?? null;
}
/**
* Map to a new collection
* @template U of object
* @param Closure(T):U $mapper
* @return Collection<U>
*/
public function map(Closure $mapper): Collection {
$result = new Collection();
foreach ($this->items as $item) {
$result->add($mapper($item));
}
return $result;
}
/**
* Filter items
* @param Closure(T):bool $predicate
* @return Collection<T>
*/
public function filter(Closure $predicate): Collection {
$result = new Collection();
foreach ($this->items as $item) {
if ($predicate($item)) {
$result->add($item);
}
}
return $result;
}
}
class User {
public function __construct(
public string $name,
public int $age
) {}
}
class UserDTO {
public function __construct(
public string $name
) {}
}
// Create a collection of users
$users = new Collection([new User("Alice", 30), new User("Bob", 25)]);
// Map to DTOs - Phan infers Collection<UserDTO>
$dtos = $users->map(fn(User $u) => new UserDTO($u->name));
// Filter users - Phan infers Collection<User>
$adults = $users->filter(fn(User $u) => $u->age >= 18);
// Type safety: this would error
// $users->add("not a user"); // ERROR: string is not object
// $users->add(new stdClass()); // ERROR: stdClass is not User