Generics mit PHP? Wie geht das denn?

Vorweg: Nein, PHP implementiert kein Generic Programming. Es gab zu diesem Thema schon heiß diskutierte RFCs und der Schrei nach Generics in PHP ist immer mal wieder zu hören. Aber bisher wurden Generics nicht in PHP umgesetzt, da diese erhebliche Änderungen in PHP selbst voraussetzen würden. Unter anderem ist hier die technische Implementierung von Arrays problematisch.

Was sind Generics?

Generics sind ein Weg der Programmierung, um sicher zu stellen, dass Konstrukte wie Collections oder Arrays nur den angegebenen Typen beinhalten. Würde PHP bereits Generics implementieren, würde es wie folgt aussehen.

<?php
declare(strict_types=1);
namespace Marcel\Test;

class ItemCollection
{
    protected array<ItemInterface> $items = [];

    public function __construct(array<ItemInterface> $items)
    {
        $this->items = $items;
    }
}

Die Collection im gezeigten Beispiel würde also nur Arrays behandeln, in denen sich ausschließlich Objekte befinden, die das ItemInterface implementieren. Es wäre natürlich ein Träumchen, wenn soetwas funktionieren würde. Tut es aber nicht. PHP bietet aber andere Möglichkeiten, um ein solches Verhalten zu realisieren.

Trotzdem Generics in PHP?

Wie man es von PHP gewohnt ist, stellt es Mittel und Wege bereit, um ein sehr ähnliches Verhalten zu realisieren. PHP stellt die Klasse SplObjectStorage bereit. Diese Klasse funktioniert bereits als Collection, die jegliche Art von Objekten beinhalten kann und bringt darüber hinaus schon alle nützlichen Methoden einer Collection mit. Wie bringen wir diese Klasse also dazu, dass sie nur die Objekte annimmt, die ein bestimmtes Interface implementieren?

<?php
declare(strict_types=1);
namespace Marcel\Test;

use InvalidArgumentException;
use SplObjectStorage;

class ItemCollection extends SplObjectStorage
{
    public function addAll(SplObjectStorage $storage): int
    {
        if (!$storage instanceof ItemCollection) {
            throw new InvalidArgumentException('Collection of items only!');
        }

        return parent::addAll($storage);
    }

    public function attach(object $object, mixed $info = null): void
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        parent::attach($object, $info);
    }

    public function detach(object $object): void
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        parent::detach($object);
    }

    public function getHash(object $object): string
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        return parent::getHash($object);
    }

    public function offsetExists($object): bool
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        return parent::offsetExists($object);
    }

    public function offsetGet($object): mixed
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        return parent::offsetGet($object);
    }

    public function offsetSet(mixed $object, mixed $info = null): void
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        parent::offsetSet($object, $info);
    }

    public function offsetUnset($object): void
    {
        if (!$object instanceof ItemInterface) {
            throw new InvalidArgumentException('Object does not implement ItemInterface');
        }

        parent::offsetUnset($object);
    }

    public function removeAll(SplObjectStorage $storage): int
    {
        if (!$storage instanceof ItemCollection) {
            throw new InvalidArgumentException('Collection of items only!');
        }

        return parent::removeAll($storage);
    }

    public function removeAllExcept(SplObjectStorage $storage): int
    {
        if (!$storage instanceof ItemCollection) {
            throw new InvalidArgumentException('Collection of items only!');
        }

        return parent::removeAllExcept($storage);
    }
}

Gut, das mag auf dem ersten Blick aufwendig aussehen. Die Erweiterung der Klasse SplObjectStorage macht aber nichts anderes, als alle relevanten, bereits vorhandenen Methoden dahingehend zu erweitern, dass auf eine ItemInterface Instanz geprüft wird. Wird keine solche Instanz übergeben, wird schlichtweg eine Exception geworfen. Somit hätten wir das Verhalten einer generischen Collection, die lediglich die angegebenen Objekte behandelt.

$itemA = new Item();
$itemB = new Item();
$itemC = new Item();
$itemD = new stdClass();

try {
    $collection = new ItemCollection();

    $collection->attach($itemA);
    $collection->attach($itemB);
    $collection->attach($itemC);
    $collection->attach($itemD);
} catch (Exception $e) {
    // $itemD wirft einen Fehler, da es keine ItemInterface Instanz ist
    echo $e->getMessage();
}

Die SplObjectStorage Klasse implementiert bereits bekannte PHP Interfaces, wie Countable, Iterator, Serializable und ArrayAccess. Da die Signatur von vererbten Methoden nicht geändert werden kann, würden Type Hints für Parameter und Return Werte unausweichlich in Fehlern enden. Aus diesem Grund wurden die von den implementierten Interfaces vererbten Methoden einfach erweitert. Mittels eines instanceof wird nun geprüft, ob es sich bei den an die Collection übergebenen Objekten tatsächlich um die Objekte handelt, die ich auch haben möchten. Zudem verzichte ich komplett auf Arrays. Warum auch ihr weniger Arrays nutzen solltet, könnt ihr hier nachlesen.

Fazit

Natürlich handelt es sich bei der gezeigten Umsetzung nicht um Generics. Aber das Verhalten der Collection ähnelt dem von Generics in etwa. Mit den gezeigten Mitteln kann man zumindest eine Collection entwickeln, die selbst sicher stellt, dass sie nur das beinhaltet, was sie auch soll.

Wie handhabt ihr solche Use Cases? Kommt sowas überhaupt bei Euch vor? Ich bin neugierig auf Eure Lösungsansätze. Schreibt ’s mir in den Kommentaren.

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.