Typing is a great way to document code, to detect some programming errors statically, and to be more confident when refactoring.

PHPStan allows to check the types in a program, and to report any typing issue. However, there is a limit: when re-using the same code with different types (like using different Collection instances with different types), we can not type our code, and we lose PHPStan’s type checking.

Introducing generic typing

Generic typing makes it possible to re-use the same piece of code with different types, while still making it statically type safe.

The idea is to declare one or more types as templates1 on a function or class. Then, use them as parameter types or return types. PHPStan will determine the real type of the templates by looking at the call site of the function, or the instantiation site of the class. It will then make sure that we use the right types consistently.

In the following example, we declare one template type named T, and use it on one parameter and the return value. When calling f(), PHPStan determines the real type of T from the passed arguments. In this example, one effect of generic typing is that the return type of f() is known, so using the return value is safer.

<?php

/**
 * @template T // Declares one template type named T 
 * @param T $x // Declares that the type of $x is T
 * @return T
 */
function f($x) {
    // here, the type of $x is the abstract type T
    return $x;
}

f(1); // PHPStan knows that this returns a int
f(new DateTime()); // PHPStan knows that this returns a DateTime

Generics in PHPStan

Because generics and PHPStan are so awesome, I’ve been working on adding the former to the later during the past weeks, with the help of PHPStan’s author Ondřej Mirtes, and the feedbacks of Psalm’s author Matt Brown.

If you install the master version of phpstan, or if you use the playground, you will be able to have a preview of generics in PHPStan.

Current state in master:

  • Generic functions: done
  • Generic closures: WIP
  • Generic classes: WIP
  • non-classname bounds: WIP
  • class-string, T::class: WIP
  • Variance: WIP
  • Prefixed annotations: WIP

Get informed on the progress by following @arnaud_lb (me), @ondrejmirtes, and @phpstan :)

Features

Bounds

Template types can be bound by using the following notation: @template <name> of <bound>. The bound is constraining the template type to be a sub-type of the specified bound.

In the following example, we bind T to DateTimeInterface. This has two distinct effects:

  • We can call greater() only with sub-types of DateTimeInterface
  • In the function body, we know that T is a sub-type of DateTimeInterface, so we can use it like a DateTimeInterface
<?php

/**
 * @template T of DateTimeInterface
 * @param T $a
 * @param T $b
 * @return T
 */
function greater($a, $b) {
    // Here, we know that $a and $b are sub-types of DateTimeInterface,
    // so it is legal to call getTimestamp() on them.
    if ($a->getTimestamp() > $b->getTimestamp()) {
        return $a;
    }
    return $b;
}

f(new DateTime("yesterday"), new DateTime("now")); // returns a DateTime
f(new DateTimeImmutable("yesterday"), new DateTimeImmutable("now")); // returns a DateTimeImmutable
f(1, 2); // error: int is not a sub type of DateTimeInterface

Nested types

Template types can be used as standalone types or as embedded types, such as in arrays, callables, iterables, etc.

<?php

/**
 * @template T
 * @param array<T> $x
 * @return T|null
 */
function first($x) {
    foreach ($x as $value) {
        return $value;
    }
    return null;
}

first([1,2,3]); // returns a int|null
<?php

/**
 * @template T
 * @param callable(T): T $a
 * @param T[] $b
 * @param iterable<T> $c
 * @param array{a: T, b: T} $d
 * @return T|null
 */
function example($a, $b, $c, $d) {
    ...
}

Propagation

Template types propagate themselves when passing them from one generic code to an other generic code:

<?php

/**
 * @template T // Declares one template type named T 
 * @param T $x // Declares that the type of $x is T
 * @return T
 */
function f($x) {
    return g($x); // Returns U, which is a T
}

/**
 * @template U
 * @param U $x
 * @return U
 */
function g($x) {
    return $x;
}

class-string<T>, T::class

Generic typing can also work with string class names, like this:

<?php

/**
 * @template T
 * @param T::class $className
 * @return T
 */
function instance(string $className) {
    return new $className();

    // This is also allowed:
    $object = getObject();
    if ($object instanceof $className) {
        return $object;
    }

    // This, too, is allowed:
    $myClassName = 'DateTime';
    if (is_subclass_of($myClassName, $className)) {
        return new $myClassName;
    }
}

instance("DateTime"); // returns a DateTime

Note: Implementation of this feature is in progress

Classes

Generic typing shows its full value when using classes. Like functions, it is possible to declare a template type on a class, and to use this type anywhere in the class definition (properties, parameters, return types).

PHPStan determines the real type of the templates by looking at the instantiation.

<?php

/** @template T */
class Collection {
    /** @var array<int,T> */
    private $elements;

    /** @param array<T> $elements */
    public function __construct(array $elements) {
        $this->elements = $elements;
    }

    /** @param T $element */
    public function add($element) {
        $this->elements[] = $element;
    }

    /** @return T */
    public function get(int $index) {
        if (!isset($this->elements[$index])) {
            throw new OutOfBoundsException();
        }
        return $this->elements[$index];
    }
}

$coll = new Collection([1,2,3]); // This is a Collection<int> : a collection of ints
$coll->add(4);
$coll->add(""); // Error
$coll->get(0); // int

Note: Implementation of this feature is in progress

Interfaces, inheritance

When implementing a generic interface, it is possible to specify its template types by using the @implements annotation:

<?php

/** @template T */
interface Collection {
    /** @return T */
    public function get();
}

/** @implements Collection<T> */
class ArrayCollection implements Collection {
    public function get() {
        // ...
    }
}

Similarly, we can use the @extends or @uses annotations for inherited classes or used traits.

The most notable effect of these annotations is that the class is known to be a sub-type of the given interface, class, or trait with the given types.

<?php

/**
 * @template T
 * @param Collection<T> $coll
 * @return T
 */
function first($coll);

$coll = new ArrayCollection([1,2,3]);

first($coll); // int

Note: Implementation of this feature is in progress

Examples

map()

<?php

/**
 * @template K The key type
 * @template V The input value type
 * @template V2 The output value type
 *
 * @param iterable<K, V> $iterable
 * @param callable(V): V2 $callback
 *
 * @return array<K, V2>
 */
function map($iterable, $callback) {
    $result = [];
    foreach ($iterable as $k => $v) {
        $result[$k] = $callback($v);
    }
    return $result;
}

Try it here.

reduce()

<?php

/**
 * @template V The input value type
 * @template V2 The output value type
 *
 * @param iterable<V> $iterable
 * @param callable(V2, V): V2 $callback
 * @param V2 $initial
 *
 * @return V2
 */
function reduce($iterable, $callback, $initial) {
    $result = $initial;
    foreach ($iterable as $v) {
        $result = $callback($result, $v);
    }
    return $result;
}

Try it here.

first()

<?php

/**
 * @template V The input value type
 *
 * @param iterable<V> $iterable
 * @param callable(V): bool $callback
 *
 * @return V|null
 */
function first($iterable, $callback) {
    foreach ($iterable as $v) {
        if ($callback($v)) {
            return $v;
        }
    }
    return null;
}

Try it here.

Acknowlegements

PHPStan is awesome, thanks to its author Ondřej Mirtes and contributors.

Also thanks to Matt Brown, author of Psalm, for actively making feedbacks.

Mention is awesome too! Thanks for letting me work on this. BTW, Mention is hiring.

Stay tuned

Get informed as we progress on generics by following @arnaud_lb (me), @ondrejmirtes, and @phpstan :)

  1. Template types, or parameterized types