Menu

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;
}
Theme
A
© 2016–2024 Ondřej Mirtes