PHPStan Generics
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 statically, 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 classes: done
- class-string<T>: done
- Prefixed annotations: done
- Generic closures: WIP
- non-classname bounds: WIP
- Variance: 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 ofDateTimeInterface
- In the function body, we know that
T
is a sub-type ofDateTimeInterface
, so we can use it like aDateTimeInterface
<?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 function to an other generic function:
<?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>
Generic typing can also work with string class names, like this:
<?php
/**
* @template T
* @param class-string<T> $className
* @return T
*/
function instance(string $className) {
// Returns a T
return new $className();
// This is also allowed:
$object = getObject();
if ($object instanceof $className) {
return $object;
}
}
instance("DateTime"); // returns a DateTime
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 E The type of elements in this collection
*/
class Collection {
/** @var array<int,E> */
private $elements;
/** @param array<E> $elements */
public function __construct(array $elements) {
$this->elements = $elements;
}
/** @param E $element */
public function add($element) {
$this->elements[] = $element;
}
/** @return E */
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
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
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 :)
-
Template types, or type variables ↩