In a nutshell:
- parse generics classes;
- generate concrete classes based on them (you can choose
monomorphization
ortype-erasure
); - autoload concrete classes instead of generics classes.
For example, you need to add several PHP files:
- generic class
Box
; - class
Usage
for use generic class; - script with composer autoload and
Usage
class.
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 with composer dump-generics
command
composer dump-generics -vv
What the composer dump-generics
command does?
- finds all generic uses in classes (
src/Usage.php
for example). - generates concrete classes from generic classes with unique names based on name and arguments of generic class.
- replaces generic class names to concrete class names in places of use.
In this case should be generated:
- 2 concrete classes of generics
BoxForInt
andBoxForString
; - 1 concrete class
Usage
with replaced generics class names to concrete class names.
Generate vendor/autoload.php with composer dump-autoload
command
composer dump-autoload
Run bin/test.php script
php bin/test.php
Composer autoload first checks the "cache" directory and then the "src" directory to load the classes.
π You can find repository with this example here.
- PHP >= 7.4
- Composer (PSR-4 Autoload)
Install library
composer require mrsuh/php-generics
Add 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/"]
}
}
}
A new class is generated for each generic argument combination.
Before monomorphization
:
<?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;
}
}
After monomorphization
:
<?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;
}
}
composer dump-generics
<?php
namespace App;
use App\Entity\Cat;
use App\Entity\Bird;
use App\Entity\Dog;
class Test extends GenericClass<Cat> implements GenericInterface<Bird> { // <-- extends/implements
use GenericTrait<Dog>; // <-- trait use
private GenericClass<int>|GenericClass<Dog> $var; // <-- property type
public function test(GenericInterface<int>|GenericInterface<Dog> $var): GenericClass<string>|GenericClass<Bird> { // <-- method argument/return type
var_dump($var instanceof GenericInterface<int>); // <-- instanceof
var_dump(GenericClass<int>::class); // <-- class constants
var_dump(GenericClass<array>::CONSTANT); // <-- class constants
return new GenericClass<float>(); // <-- new
}
}
<?php
namespace App;
class Test<T,V> extends GenericClass<T> implements GenericInterface<V> { // <-- extends/implements
use GenericTrait<T>; // <-- trait use
use T; // <-- trait use
private T|GenericClass<V> $var; // <-- property type
public function test(T|GenericInterface<V> $var): T|GenericClass<V> { // <-- method argument/return type
var_dump($var instanceof GenericInterface<V>); // <-- instanceof
var_dump($var instanceof T); // <-- instanceof
var_dump(GenericClass<T>::class); // <-- class constants
var_dump(T::class); // <-- class constants
var_dump(GenericClass<T>::CONSTANT); // <-- class constants
var_dump(T::CONSTANT); // <-- class constants
$obj1 = new T(); // <-- new
$obj2 = new GenericClass<V>(); // <-- new
return $obj2;
}
}
π You can read more about monomorphization
here.
A new class is generated without generics arguments.
Before type erasure
:
<?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;
}
}
After type erasure
:
<?php
namespace App;
class Box {
private $data = null;
public function set($data) : void
{
$this->data = $data;
}
public function get()
{
return $this->data;
}
}
composer dump-generics --type=type-erasure
<?php
namespace App;
use App\Entity\Cat;
use App\Entity\Bird;
use App\Entity\Dog;
class Test extends GenericClass<Cat> implements GenericInterface<Bird> { // <-- extends/implements
use GenericTrait<Dog>; // <-- trait use
private GenericClass<int>|GenericClass<Dog> $var; // <-- property type
public function test(GenericInterface<int>|GenericInterface<Dog> $var): GenericClass<string>|GenericClass<Bird> { // <-- method argument/return type
var_dump($var instanceof GenericInterface<int>); // <-- instanceof
var_dump(GenericClass<int>::class); // <-- class constants
var_dump(GenericClass<array>::CONSTANT); // <-- class constants
return new GenericClass<float>(); // <-- new
}
}
<?php
namespace App;
class Test<T,V> extends GenericClass<T> implements GenericInterface<V> { // <-- extends/implements
use GenericTrait<T>; // <-- trait use
private GenericClass<V> $var; // <-- property type
public function test(T|GenericInterface<V> $var): T|GenericClass<V> { // <-- method argument/return type
var_dump($var instanceof GenericInterface<V>); // <-- instanceof
var_dump(GenericClass<T>::class); // <-- class constants
var_dump(GenericClass<T>::CONSTANT); // <-- class constants
return new GenericClass<V>(); // <-- new
}
}
π You can read more about type-erasure
here.
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 {
}
}
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')); // is it generic?
var_dump( (new \DateTime < FOO) , ( BAR > 'now') ); // no, it is not
Therefore, nested generics are not currently supported.
<?php
namespace App;
class Usage {
public function run() {
$map = new Map<Key<int>, Value<string>>();//not supported
}
}
<?php
namespace App;
class GenericClass<T, varType, myCoolLongParaterName> {
private T $var1;
private varType $var2;
private myCoolLongParaterName $var3;
}
<?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;
}
}
<?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<>();//be sure to add "<>"
$map->set('key', 1);
var_dump($map->get('key'));
}
}
All concrete classes are pre-generated and can be cached(should not affect performance).
Generating many concrete classes should negatively impact performance when:
- resolves concrete classes;
- storing concrete classes in memory;
- type checking for each concrete class.
I think it's all individual for a specific case.
Autoload magic of concrete classes works with composer autoload only.
Nothing will work because of syntax error if you include file by "require"
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.
composer test
- Add directory 00-your-dir-name to ./tests/{monomorphic/type-erased}
- Generate output files and check it
php bin/generate.php monomorphic tests/monomorphic/000-your-dir-name
php bin/generate.php type-erased tests/type-erased/000-your-dir-name