stratadox/php-generics

v0.1 2020-03-04 23:03 UTC

This package is auto-updated.

Last update: 2024-03-05 08:13:57 UTC


README

Build Status Coverage Status Scrutinizer Code Quality

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 import Foo\Bar, you could build a Collection__Bar as typed list of Foo\Bar objects. If, in another file in the same namespace, Baz\Bar were used and a Collection__Bar were instantiated, the collection may or may not reject the Baz\Bar objects, depending on whether or not Collection__Bar was already loaded as a collection of Foo\Bar objects. This limitation can be circumvented by using an alias, e.g. use Baz\Bar as BazBar, and accordingly Collection__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 class SpecialThing, one may pass a SpecialThing into a Collection__Thing. The Collection__SpecialThing, however, does not extend from Collection__Thing. When a Collection__Thing is required, the Collection__SpecialThing will not satisfy.

    The reason for the limitation is that a Map__SpecialThing__SpecialThing would have to extend both Map__Thing__SpecialThing and Map__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.