Generic types are templates which allow us to write the code without specifying a particular type of data on which the code will work. Thanks to them, we avoid the redundancy and the objects operate on the previously declared types. A good example here are collections of various types. If we want to be sure that a collection consists of a given data type, we can either create a separate class to store each type, use various types of assertions, or just use generic types.
In case of separate collection classes we create redundancy - we duplicate classes that differ only in the type of stored object. In case of assertions, the IDE will not be able to tell us the syntax. Only generic types will allow us to create one class that will guarantee data consistency and correct IDE autocompleting.
How to deal with it in PHP
In theory, everything looks nice. There is only one small problem - PHP does not have built-in generic types. Although a few years ago there was the RFC, but it was not implemented. Fortunately, there are tools for static code analysis, such as Psalm or PHPStan, which, by reading appropriate annotations in the code, are able to imitate the generic types.
We can easily integrate them with a popular IDEs such as PHPStorm, and they will inform us about incorrectly passed or returned data from other objects. Additionally, it will allow PHPStorm to guess which types will be returned by particular methods and this will enable autocomplete functionality.
Of course, it may happen that not everyone has static analysis tools configured in their IDE, and therefore, someone will pass a type other than declared, and the IDE will not catch it. To avoid such situations, we can also integrate static code analysis with CI/CD process. In this way, any violations of the rules will be caught at the stage of building the application and will not go to production.
Examples
If we want our class to operate on generic types, we need to add the @template
annotation to it along with the name of the parameter to be generic. Then, instead of using the built-in DocBlock types, we use the type declared in the @template
annotation. This is what the example of the Collection interface using generic type looks like:
<?php
interface Entity { }
class Car implements Entity
{
public function __construct(
public readonly int $id,
public readonly float $engineCapacity
) {}
}
/**
* @template T
*/
interface Collection
{
/**
* @param T $entity
*/
public function add($entity);
/**
* @param int $id
* @return T
*/
public function find(int $id);
}
When trying to call a method on such a collection, the IDE will be able to recognise which object we expect and suggest its methods or properties.
However, there may always be some omissions, and sometimes we may try to pass an object of a different type than the declared one. Then the IDE, thanks to static code analysis, will detect the problem and notify us about the type violation.
class Animal {}
Types can be also restricted to subclasses of a class or interface. We can do this by appending of SomeClass
to the type expression. Then, the following collection can consist of classes that extend or implement Entity.
/**
* @template T of Entity
*/
interface Collection
{
//...
}
Extending templates
Above we have created an interface. Now we create a class that implements this interface. For this purpose, in addition to the @template
annotation, we add @template-implements Collection<T>
.
/**
* @template T of Entity
* @template-implements Collection<T>
*/
class ArrayCollection implements Collection
{
/**
* @var array<int, T>
*/
private array $values = [];
/**
* @param T $entity
*/
public function add($entity)
{
$this->values[] = $entity;
}
/**
* @param int $id
* @return T
*/
public function find($id)
{
return $this->values[$position];
}
}
The same applies to extending classes - we use the @template-extends ArrayCollection<T>
annotation
/**
* @template T of Entity
* @template-extends ArrayCollection<T>
*/
class SpecificArrayCollection extends ArrayCollection {}
If in the SpecificArrayCollection class we tried to override the add
method so that it takes a different type, such as int, then Psalm would show us an error.
We can also extend a generic class and declare it to accept a specific data type, in this case Car.
/**
* @template-extends ArrayCollection<Car>
*/
class CarArrayCollection extends ArrayCollection {}
The CarArrayCollection class will be able to accept only elements of the Car class.
Guessing object by class name
Psalm provides special annotations for class constants, e.g. AClass::class
. This can be useful in Container or ServiceLocator services, where we want to get an instance of the class based on the class name.
To achieve that, we add the @template
annotation above the method that should take the class name. To specify the parameter type we use @param class-string<T> $className
. If we specify T as the return type, the IDE will know that we are dealing with an instance of this class and will give us hints about the available methods, as well as pointing out their incorrect usage.
class ServiceA { public string $serviceAProperty; }
class ServiceB { public string $serviceBProperty; }
class ServiceLocator
{
/**
* @template T
* @param class-string<T> $className
* @return T
*/
public function getService(string $className)
{
return new $className;
}
}
Multiple generic types
The above examples show how to use a single generic type in particular classes. However, we often need several of them. In that case we simply add another @template
annotation with the name of the next generic type. The following example shows a simple implementation of a HashMap, where the key is an allowed array key and the value is a generic element.
/**
* @template TKey of array-key
* @template T
*/
class HashMap
{
/**
* @var array<TKey, T>
*/
private array $values = [];
/**
* @param TKey $key
* @param T $entity
*/
public function set($key, $entity)
{
$this->values[$key] = $entity;
}
/**
* @param TKey $key
* @return T
*/
public function get($key)
{
return $this->values[$key];
}
}
Generic types in callback functions
The annotations described above can be also used in callback functions. This way we can avoid accidentally passing a function that will take or return values other than those declared.
/**
* @template T
* @template R
*/
class CallbackExecutor
{
/**
* @param callable(T):R $callable
* @param T $value
* @return R
*/
public function execute(callable $callable, mixed $value)
{
return $callable($value);
}
}
Code analysis from the command line
The above examples of type anomaly detection apply to the IDE, which in this case is PHPStorm. It has been specially configured to check the code being written on the fly. Of course, everyone can use different IDE to work, and not all of them allow such integration. Then, a good way might be the traditional code checking done by running for example Psalm from the command line. The result of such a command may look like this:
root@2fde6041ad38:/app# vendor/bin/psalm public/test.php
Target PHP version: 8.1 (inferred from current PHP version)
Scanning files...
Analyzing files...
E
ERROR: UndefinedPropertyFetch - public/test.php:29:1 - Instance property ServiceB::$serviceAProperty is not defined (see https://psalm.dev/039)
$serviceLocator->getService(ServiceB::class)->serviceAProperty;
ERROR: InvalidScalarArgument - public/test.php:88:28 - Argument 1 of CallbackExecutor::execute expects callable(int):string, pure-Closure(string):float provided (see https://psalm.dev/012)
$callbackExecutor->execute(fn(string $value) => (float) $value, 1);
------------------------------
2 errors found
------------------------------
3 other issues found.
You can display them with --show-info=true
------------------------------
Psalm can automatically fix 1 of these issues.
Run Psalm again with
--alter --issues=MissingReturnType --dry-run
to see what it can fix.
------------------------------
Checks took 0.49 seconds and used 75.842MB of memory
Psalm was able to infer types for 98.6486% of the codebase
Summary
Although the PHP is developing quite dynamically, it does not support native generic types, which are widely used in languages such as Java or C#. Fortunately, there are many tools that allow us to imitate these types, and thus, extend the capabilities of the language.
They allow us to reduce code duplication in the application, and make us sure that the values we pass are of the type we expect. The development of the most popular IDE, which is PHPStorm, allows integration with tools such as Psalm, PHPStan, which enables auto-complete, as well as error detection at the editor level.
Integrating static code analysis tools with CI/CD will help us to detect anomalies at the application build stage and prevent them from being deployed into production environment.
Top comments (2)
It is a shame that we have php 8 and need to use doc comments to achive this
Thanks