stratadox / php-generics
Requires
- php: >=7.2
- doctrine/annotations: ^1.8
- nikic/php-parser: ^4.3
- psr/log: ^1.1
- stratadox/immutable-collection: ^1.1
Requires (Dev)
- php-coveralls/php-coveralls: ^2.1
- phpstan/phpstan: ^0.11.12
- phpunit/phpunit: ^8.2
- roave/security-advisories: dev-master
This package is auto-updated.
Last update: 2024-11-05 09:53:15 UTC
README
Albeit with some limitations and caveats, this library unlocks the power of generics for the PHP language.
Installation
Install with composer require stratadox/php-generics
Usage
First install the autoloader, using:
<?php use Stratadox\PhpGenerics\Autoloader; Autoloader::default()->install();
Making a new instance of a generic class
Given a generic base class Collection
, with type parameter T
.
To make a collection of strings, use:
<?php namespace My\Name\Space; use My\Generic\Collection; var_dump(new Collection__string('foo', 'bar'));
To make a collection of Foo, use:
<?php namespace My\Name\Space; use My\Generic\Collection; use My\Domain\Foo; var_dump(new Collection__Foo(new Foo(), new Foo()));
Don't worry if the classes Collection__string
and Collection__Foo
don't
exist (yet), they will be generated by the autoloader as soon as the code in
question is executed.
There's no need to import either Collection__string
or Collection__Foo
in
the aforementioned examples; the generated classes adopt the namespace of the
calling code.
The generic base class (in this case My\Generic\Collection
) and the type
arguments (My\Domain\Foo
) do need to be imported, even if they are not
(seemingly) used.
Only when type checking for the concrete generic class, the import is required:
<?php namespace Some\Other\Name\Space; use My\Name\Space\Collection__Foo; function doSomething(Collection__Foo $list) { // ... }
Generics as type arguments
You can combine generics; for example:
<?php namespace My\Name\Space; use My\Generic\Collection; use My\Domain\Foo; var_dump(new Collection__Collection__Foo( new Collection__Foo(new Foo(), new Foo()), new Collection__Foo(new Foo(), new Foo()), ));
Making a new generic base class
To make a generic class, use the @Generic
annotation. The annotation takes a
count
parameter, indicating the amount of generic types. You can use any class
name in the Stratadox\PhpGenerics\Generic
namespace as type parameter.
For example:
<?php namespace My\Generic; use Stratadox\PhpGenerics\Annotation\Generic; use Stratadox\PhpGenerics\Generic\T;use function assert; /** @Generic(count=1) */ class Collection extends \ArrayObject { public function __construct(T ...$items) { parent::__construct($items); } public function offsetSet($index, $value): void { assert($value instanceof T); parent::offsetSet($index, $value); } }
Enforcing return types
Php classes cannot alter their parent's return types if they have one, so it's
not possible to declare a return type directly in the generic base class.
Using the @ReturnType
annotation, however, will resolve this issue:
<?php namespace My\Generic; use ArrayObject; use Stratadox\PhpGenerics\Annotation\Generic; use Stratadox\PhpGenerics\Annotation\ReturnType; use Stratadox\PhpGenerics\Generic\T; /** @Generic(count=1) */ class Collection extends ArrayObject { public function __construct(T ...$items) { parent::__construct($items); } public function offsetSet($index, $value): void { assert(is_int($index) || null === $index); assert($value instanceof T); parent::offsetSet($index, $value); } /** @ReturnType(force="T") */ public function offsetGet($index) { return parent::offsetGet($index); } }
Generated concrete class
When the generic base class above is present and imported, an instantiation of
Collection__string
from within My\Name\Space
will produce a file with the
following contents:
<?php namespace My\Name\Space; use ArrayObject; use My\Generic\Collection; use Stratadox\PhpGenerics\Annotation\ReturnType; final class Collection__string extends Collection { public function __construct(string ...$items) { ArrayObject::__construct($items); } public function offsetSet($index, $value): void { assert(is_int($index) || null === $index); assert(is_string($value)); ArrayObject::offsetSet($index, $value); } /** @ReturnType(force="T") */ public function offsetGet($index): string { return ArrayObject::offsetGet($index); } }
Inner workings
This library uses some of the darkest magic available to php. In short, it hacks into the auto-loading process, adding a loader that peeks into the stack trace and subsequently dissects the source code of the file that instantiated the generic class, in order to harvest the namespace and imports and produce a new concrete class that satisfies the type arguments. Generating the class involves even more magic, in the sense that the generic base class is parsed into an abstract syntax tree, modified to accommodate the type arguments and transformed back again into php code, to be written to a file somewhere.
TL;DR: Black magic.
Limitations
-
The
<
and>
symbols are (for now?) illegal for use in class names in php, even when cheating. Please prove me wrong. Until you do, we'll have to make do with legal characters, the default currently being a double underscore. -
You can use each type combination only for that specific type combination per namespace. If that did not sound confusing I'll eat my hat, so let me explain:
- A concrete class is generated for a combination of generic base class and type arguments
- The concrete class takes the namespace it was created in
- The type arguments' FQCN's are extracted from the imports of the calling file
- Type arguments are referenced by their short name in the concrete class name
Following from the above, if you're in the namespace
Acme\Foo
and importFoo\Bar
, you could build aCollection__Bar
as typed list ofFoo\Bar
objects. If, in another file in the same namespace,Baz\Bar
were used and aCollection__Bar
were instantiated, the collection may or may not reject theBaz\Bar
objects, depending on whether or notCollection__Bar
was already loaded as a collection ofFoo\Bar
objects. This limitation can be circumvented by using an alias, e.g.use Baz\Bar as BazBar
, and accordinglyCollection__BazBar
. -
No inheritance in generic types. Although you can pass subtypes into a type hinted concrete class, a collection of subclasses will not inherit from the collection of parents.
Given a parent class
Thing
and a child classSpecialThing
, one may pass aSpecialThing
into aCollection__Thing
. TheCollection__SpecialThing
, however, does not extend fromCollection__Thing
. When aCollection__Thing
is required, theCollection__SpecialThing
will not satisfy.The reason for the limitation is that a
Map__SpecialThing__SpecialThing
would have to extend bothMap__Thing__SpecialThing
andMap__SpecialThing__Thing
, but multiple inheritance isn't allowed. A potential solution / bypass could be to generate and implement both_Map__Thing__SpecialThing
and_Map__SpecialThing__Thing
(and_Map_Thing_Thing
), so that type checks would be possible albeit with prefix. Might want to have that before v1.0.