Generics By Examples
March 14, 2022 · 10 min read
There’s a comprehensive guide on generics in PHP using PHPDocs, but after reading it someone might still not be sure where to begin.
That’s why I collected these examples of generics in practice. It’s here to give you an idea how to actually use generics in real-world code every day.
Return the same type the function accepts #
/**
* @template T
* @param T $p
* @return T
*/
function foo($p)
{
// ...
}
Return the same type the function accepts and limit what the type can be #
The of Foo
part is called “template type variable bound”.
/**
* @template T of Foo
* @param T $p
* @return T
*/
function foo(Foo $p): Foo
{
// ...
}
Accept a class string and return the object of that type #
/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
function foo(string $className): object
{
// ...
}
Accept a class string and return an array of objects of that type #
/**
* @template T of object
* @param class-string<T> $className
* @return array<int, T>
*/
function foo(string $className): array
{
// ...
}
Accept an array and return an array with the same key type #
/**
* @template TKey of array-key
* @param array<TKey, mixed> $p
* @return array<TKey, mixed>
*/
function foo(array $p): array
{
// ...
}
Define a generic interface #
/**
* @template T
*/
interface Collection
{
/** @param T $item */
public function add($item): void;
/** @return T */
public function get();
}
Specify template type variable of a generic interface or class in a parameter type #
Specifying the template type with Collection<Foo>
will get you rid of this error:
Function foo() has parameter $c with generic interface Collection but does not specify its types: T
/**
* @param Collection<Foo> $c
*/
function foo(Collection $c): void
{
// ...
}
Specify template type variable of a generic interface when implementing it #
Specifying the template type with @implements Collection<Foo>
will get you rid of this error:
Class Bar implements generic interface Collection but does not specify its types: T
/** @implements Collection<Foo> */
class Bar implements Collection
{
}
If you want the child class to be also generic, repeat @template
above it:
/**
* @template T
* @implements Collection<T>
*/
class Bar implements Collection
{
}
The child class can also have different number and names of @template
tags.
Specify template type variable of a generic class when extending it #
The @extends
tag needs to be used instead of @implements
:
/** @extends Collection<Foo> */
class Bar extends Collection
{
}
This will get you rid of the following error:
Class Bar extends generic class Collection but does not specify its types: T
Function accepts any string, but returns object of the same type if it’s a class-string #
Typical scenario for the service locator (dependency injection container) pattern.
The following code doesn’t work because string|class-string<T>
is normalized to string
:
class Container
{
/**
* @template T of object
* @param string|class-string<T> $name
* @return ($name is class-string ? T : object)
*/
public function get(string $name): object
{
// ...
}
}
But the following code works as expected:
class Container
{
/**
* @template T of object
* @return ($name is class-string<T> ? T : object)
*/
public function get(string $name): object
{
// ...
}
}
If your container only holds services that are named with their FQCN you could write it like:
class Container
{
/**
* @template T of object
* @param class-string<T> $name
* @return T
*/
public function get(string $name): object
{
// ...
}
}
Container with static services #
If you have a container with a static array of services, you can use new
to return the correct type like this:
class Container
{
/**
* @var array<string, class-string>
*/
private const TYPES = [
'foo' => DateTime::class,
'bar' => DateTimeImmutable::class,
];
/**
* @template T of key-of<self::TYPES>
* @param T $type
*
* @return new<self::TYPES[T]>
*/
public static function get(string $type) : object
{
$class = self::TYPES[$type] ?? throw new InvalidArgumentException('Not found');
return new $class();
}
}
Couple relevant classes together #
The following example is especially useful when you want to couple together things like an Event + its EventHandler.
Let’s say you have the following interfaces and classes:
interface Original
{
}
interface Derivative
{
}
class MyOriginal implements Original
{
}
class MyDerivative implements Derivative
{
}
And you have a function that returns MyDerivative
when MyOriginal
is given:
function foo(Original $o): Derivative
{
// ...
}
PHPStan out of the box unfortunately can’t know that:
$d = foo(new MyOriginal());
\PHPStan\dumpType($d); // Derivative, not MyDerivative :(
But you can couple those relevant classes together with the help of generics. First, you have to make Original generic so that it’s aware of its derivative:
/** @template TDerivative of Derivative */
interface Original
{
}
After that you need to say that MyOriginal
is coupled to MyDerivative
:
/** @implements Original<MyDerivative> */
class MyOriginal implements Original
{
}
And you need to modify the signature of the foo()
function to “extract” the Derivative
from the given Original
:
/**
* @template T of Derivative
* @param Original<T> $o
* @return T
*/
function foo(Original $o): Derivative
{
// ...
}
And once you do all that, the type in the following code is correctly inferred:
$d = foo(new MyOriginal());
\PHPStan\dumpType($d); // MyDerivative :)
Link to this example on the playground »
Specify template type variable of a generic trait when using it #
Traits can be generic too:
/** @template T of Foo */
trait Part
{
/** @param T $item */
public function bar(Foo $item): void
{
// ...
}
}
Into a class using a generic trait, the @use
tag can be defined on the PHPDoc of the use clause:
/** @template T of Foo */
class Bar
{
/** @use Part<T> */
use Part;
}