Skip to content

Latest commit

 

History

History
1272 lines (1000 loc) · 42 KB

File metadata and controls

1272 lines (1000 loc) · 42 KB

Generic Types in Phan v6

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-covariant and @template-contravariant
  • Utility types: key-of<T>, value-of<T>, int-range<min, max>, positive-int, negative-int

Table of Contents

  1. Basic Generic Classes
  2. Generic Interfaces
  3. Generic Traits
  4. Template Constraints
  5. Variance Annotations
  6. Function Templates
  7. Utility Types
  8. Rules and Limitations

Basic Generic Classes

Declaring Template Types

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 string

Try this example in Phan-in-Browser →

Constructor Must Define All Template Types

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.

Extending Generic Classes

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 →

Generic Interfaces

New in v6: Phan now supports generic interfaces using the @implements annotation.

Declaring Generic Interfaces

/**
 * @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 →

Implementing Generic Interfaces

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 User

Try this example in Phan-in-Browser →

Multiple Interfaces

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 →

Nested Generic Types

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 →

Alternative: @phan-implements

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 →

Generic Traits

New in v6: Phan now supports generic traits using the @use annotation.

Declaring Generic Traits

/**
 * @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 →

Using Generic Traits

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 →

Combining Class Templates with Trait Templates

/**
 * @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 int

Try this example in Phan-in-Browser →

Alternative: @phan-use

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 →

Template Constraints

New in v6: Template parameters can be constrained to specific types using @template T of SomeClass.

Basic Constraints

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 provided

Constraints with Interfaces

interface 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 →

Constraints on Functions

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: PhanTemplateTypeConstraintViolation

Union Type Constraints

Constraints 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|string

Try this example in Phan-in-Browser →

Intersection Type Constraints

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 →

Variance Annotations

New in v6: Phan supports variance annotations to enforce proper template usage in read/write positions.

Covariant Templates

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: PhanTemplateTypeVarianceViolation

Covariant 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 it

Try this example in Phan-in-Browser →

Contravariant Templates

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: PhanTemplateTypeVarianceViolation

Contravariant 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 it

Try this example in Phan-in-Browser →

Invariant Templates (Default)

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 →

Variance and Properties

New in v6: Phan enforces variance rules on properties:

  • Covariant templates are only allowed on readonly or @phan-read-only properties
  • 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 →

Function Templates

Templates can be inferred from function and method parameters, useful for inferring return types.

Basic Function Templates

/**
 * @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 int

Try this example in Phan-in-Browser →

Templates from Closure Return Types

/**
 * @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 →

Multiple Template Parameters

/**
 * @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 →

class-string<T> Type

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 Article

Try this example in Phan-in-Browser →

Utility Types

New in v6: Phan supports several utility types for more precise type definitions.

key-of<T>

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 key

Try this example in Phan-in-Browser →

Test this example:

~/phan/phan -n example.php
# Expected error: PhanTypeMismatchArgumentProbablyReal

With 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 →

value-of<T>

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|string

Try this example in Phan-in-Browser →

Test this example:

~/phan/phan -n example.php
# Expected error: PhanTypeMismatchArgument

int-range<min, max>

Define 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 maximum

Try this example in Phan-in-Browser →

Test this example:

~/phan/phan -n example.php
# Expected errors: PhanTypeMismatchArgument for out-of-range values

Ranges 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);   // ERROR

Try this example in Phan-in-Browser →

positive-int and negative-int

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 negative

Try this example in Phan-in-Browser →

Test this example:

~/phan/phan -n example.php
# Expected errors for invalid values

Rules and Limitations

Constructor Rule

All 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.

No Generic Statics

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 →

Template Parameter Count

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.

Template Bound Inheritance

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 →

Variance and Nested Structures

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).

Performance Considerations

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

Example: Complete Generic Data Structure

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

Try this example in Phan-in-Browser →

Further Reading