PHP Generics. Right here. Right now.

Many PHP developers, including me, would like to see support for generics in PHP. An RFC for adding generics was created in 2016, but it hasn’t been finished yet.
I looked into different ways to add generics to PHP but couldn’t find a solution that works well for regular developers. So, I decided to create my own solution in PHP. If you want to try it, you can use this library mrsuh/php-generics and check out the repo to experiment with it.

Quote

How it works?

In a nutshell:

Detailed algorithm.

Install library with composer (PHP >= 7.4)

composer require mrsuh/php-generics

Add one more directory("cache/") to composer autoload PSR-4 for generated classes.
It should be placed before the main directory.
composer.json

{
   "autoload": {
       "psr-4": {
           "App\\": ["cache/","src/"]
       }
   }
}

For example, you need add several PHP files:

src/Box.php

<?php

namespace App;

class Box<T> {

   private ?T $data = null;

   public function set(T $data): void {
       $this->data = $data;
   }

   public function get(): ?T {
       return $this->data;
   }
}

src/Usage.php

<?php

namespace App;

class Usage {

   public function run(): void
   {
       $stringBox = new Box<string>();
       $stringBox->set('cat');
       var_dump($stringBox->get()); // string "cat"

       $intBox = new Box<int>();
       $intBox->set(1);
       var_dump($intBox->get()); // integer 1
   }
}

bin/test.php

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use App\Usage;

$usage = new Usage();
$usage->run();

Generate concrete classes from generic classes by composer dump-generics

composer dump-generics -v
Generating concrete classes
 - App\BoxForString
 - App\BoxForInt
 - App\Usage
Generated 3 concrete classes in 0.062 seconds, 16.000 MB memory used

What the composer dump-generics command does?

In this case should be generated:

cache/BoxForInt.php

<?php

namespace App;

class BoxForInt
{
   private ?int $data = null;

   public function set(int $data) : void
   {
       $this->data = $data;
   }

   public function get() : ?int
   {
       return $this->data;
   }
}

cache/BoxForString.php

<?php

namespace App;

class BoxForString
{
   private ?string $data = null;

   public function set(string $data) : void
   {
       $this->data = $data;
   }

   public function get() : ?string
   {
       return $this->data;
   }
}

cache/Usage.php

<?php

namespace App;

class Usage
{
   public function run() : void
   {
       $stringBox = new \App\BoxForString();
       $stringBox->set('cat');
       var_dump($stringBox->get());// string "cat"

       $intBox = new \App\BoxForInt();
       $intBox->set(1);
       var_dump($intBox->get());// integer 1
   }
}

Generate vendor/autoload.php with composer dump-autoload command

composer dump-autoload
Generating autoload files
Generated autoload files

Run script

php bin/test.php

Composer autoload first checks the "cache" directory and then the "src" directory to load the classes.
You can find the code for this example here
More examples here

Implementation features

What syntax is used?

The RFC does not define a specific syntax so i took this one implemented by Nikita Popov

Syntax example:

<?php

namespace App;

class Generic<in T: Iface = int, out V: Iface = string> {

   public function test(T $var): V {

   }
}

Syntax problems

I had to upgrade nikic/php-parser for parse code with new syntax.
You can see here the grammar changes that had to be made for support generics.

Parser use PHP implementation of YACC.
The YACC(LALR) algorithm and current PHP syntax make it impossible to describe the full syntax of generics due to collisions.

Collision example:

<?php

const FOO = 'FOO';
const BAR = 'BAR';

var_dump(new \DateTime<FOO, BAR>('now')); // кажется, что здесь есть дженерик
var_dump( (new \DateTime < FOO) , ( BAR > 'now') ); // на самом деле нет

Solution options

Therefore, nested generics are not currently supported.

<?php

namespace App;

class Usage {
   public function run() {
       $map = new Map<Key<int>, Value<string>>();//не поддерживается
   }
}

Parameter names have not special restrictions

<?php

namespace App;

class GenericClass<T, varType, myCoolLongParaterName> {
   private T $var1;
   private varType $var2;
   private myCoolLongParaterName $var3;   
}

Several generic parameters support

<?php

namespace App;

class Map<keyType, valueType> {

   private array $map;

   public function set(keyType $key, valueType $value): void {
       $this->map[$key] = $value;
   }

   public function get(keyType $key): ?valueType {
       return $this->map[$key] ?? null;
   }
}

Default generic parameter support

<?php

namespace App;

class Map<keyType = string, valueType = int> {

   private array $map = [];

   public function set(keyType $key, valueType $value): void {
       $this->map[$key] = $value;
   }

   public function get(keyType $key): ?valueType {
       return $this->map[$key] ?? null;
   }
}
<?php

namespace App;

class Usage {
   public function run() {
       $map = new Map<>();//обязательно нужно добавить знаки "<>"
       $map->set('key', 1);
       var_dump($map->get('key'));
   }
}

Where in class can generics be used?

An example of class that uses generics:

<?php

namespace App;

use App\Entity\Cat;
use App\Entity\Bird;
use App\Entity\Dog;

class Test extends GenericClass<Cat> implements GenericInterface<Bird> {

  use GenericTrait<Dog>;

  private GenericClass<int>|GenericClass<Dog> $var;

  public function test(GenericInterface<int>|GenericInterface<Dog> $var): GenericClass<string>|GenericClass<Bird> {

       var_dump($var instanceof GenericInterface<int>);

       var_dump(GenericClass<int>::class);

       var_dump(GenericClass<array>::CONSTANT);

       return new GenericClass<float>();
  }
}

Where in generic class can parameters be used?

And example of generic class:

<?php

namespace App;

class Test<T,V> extends GenericClass<T> implements GenericInterface<V> {

  use GenericTrait<T>;
  use T;

  private T|GenericClass<V> $var;

  public function test(T|GenericInterface<V> $var): T|GenericClass<V> {

       var_dump($var instanceof GenericInterface<V>);

       var_dump($var instanceof T);

       var_dump(GenericClass<T>::class);

       var_dump(T::class);

       var_dump(GenericClass<T>::CONSTANT);

       var_dump(T::CONSTANT);

       $obj1 = new T();
       $obj2 = new GenericClass<V>();

       return $obj2;
  }
}

How fast is it?

All concrete classes are pre-generated and can be cached(should not affect performance).

Generating many concrete classes should negatively impact performance when:

I think it's all individual for a specific case.

Doesn't work without composer autoload

Autoload magic of concrete classes works with composer autoload only.
Nothing will not work because of syntax error if you include file by "require"
PhpUnit include test files by "require" only because of its own reasons
Therefore you can't use generic classes with PhpUnit.

Reflection

PHP does type checks in runtime.
Therefore, all generics arguments must me available through reflection in runtime.
It can't be, because information about generics arguments is erased after concrete classes are generated.

IDE

Reflection

PHP выполняет проверки типов в runtime. Значит, все аргументы дженериков должны быть доступны через reflection в runtime. А этого не может быть, потому что информация о аргументах дженериков после генерации конкретных классов стирается.

What is Not Implemented According to the RFC

Generics for Functions, Anonymous Functions, and Methods

<?php

namespace App;

function foo<T,V>(T $arg): V {

}

Type Checking for Generic Parameters

The type T must be a subclass of or implement the interface TInterface.

<?php

namespace App;

class Generic<T: TInterface> {

}

Variance of Parameters

<?php

namespace App;

class Generic<in T, out V> {

}

Psalm Template Annotations
Features:

Existing Solutions in PHP

Psalm Template Annotations

Features:

<?php
/**
* @template T
*/
class MyContainer {
 /** @var T */
  private $value;

/** @param T $value */
public function __construct($value) {
  $this->value = $value;
}

/** @return T */
public function getValue() {
  return $this->value;
}
}

spatie/typed

Features:

<?php

$list = new Collection(T::bool());

$list[] = new Post(); // TypeError
<?php

$point = new Tuple(T::float(), T::float());

$point[0] = 1.5;
$point[1] = 3;

$point[0] = 'a'; // TypeError
$point['a'] = 1; // TypeError
$point[10] = 1; // TypeError

TimeToogo/PHP-Generics

Features:

<?php

class Maybe {
private $MaybeValue;

public function __construct(__TYPE__ $Value = null) {
$this->MaybeValue = $Value;
}

public function HasValue() {
return $this->MaybeValue !== null;
}

public function GetValue() {
return $this->MaybeValue;
}

public function SetValue(__TYPE__ $Value = null) {
$this->MaybeValue = $Value;
}
}
<?php

$Maybe = new Maybe\stdClass();
$Maybe->HasValue(); //false
$Maybe->SetValue(new stdClass());
$Maybe->HasValue(); //true
$Maybe->SetValue(new DateTime()); //ERROR
<?php

$Configuration = new \Generics\Configuration();
$Configuration->SetIsDevelopmentMode(true);
$Configuration->SetRootPath(__DIR__);
$Configuration->SetCachePath(__DIR__ . '/Cache');
//Register the generic auto loader
\Generics\Loader::Register($Configuration);

ircmaxell/PhpGenerics

Features:

Test/Item.php

<?php

namespace test;

class Item<T> {

protected $item;

public function __construct(T $item = null)
{
$this->item = $item;
}

public function getItem()
{
return $item;
}

public function setItem(T $item)
{
$this->item = $item;
}
}

Test/Test.php

<?php

namespace Test;

class Test {
   public function runTest()
   {
       $item = new Item<StdClass>;
       var_dump($item instanceof Item); // true
       $item->setItem(new StdClass); // works fine
       // $item->setItem([]); // E_RECOVERABLE_ERROR
   }
}

test.php

<?php

require "vendor/autoload.php";

$test = new Test\Test;
$test->runTest();

Differences from mrsuh/php-generics:

Conclusion

I think I have achieved what I wanted: the library is easy to install and can be used in real projects. What is frustrating, however, is that, for understandable reasons, popular IDEs don't fully support the new generics syntax, so it's currently difficult to use it.
If you have suggestions or questions, feel free to leave them here.