DEV Community

Jarek
Jarek

Posted on

How To Start Using Generic Types In PHP

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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 {}
Enter fullscreen mode Exit fullscreen mode

Image description

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
{
    //...
}
Enter fullscreen mode Exit fullscreen mode

Image description

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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 {}
Enter fullscreen mode Exit fullscreen mode

The CarArrayCollection class will be able to accept only elements of the Car class.

Image description

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

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

Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
atabak profile image
atabak

Thanks