SplObjectStorage: Objekte als Map-Keys praktisch nutzen
AI generated
8.4
PHP · SPL · Datenstrukturen
SplObjectStorage: Objekte als Schlüssel nutzen
Die Lösung für ein strukturelles Array-Problem

PHP-Arrays akzeptieren als Schlüssel nur Integer und Strings, Objekte werden dabei stillschweigend in Fehler oder in einen unbrauchbaren String konvertiert. SplObjectStorage schließt diese Lücke, indem sie Objekte per Identität als Schlüssel zulässt. Wir zeigen anhand von Objekt-Metadaten und Graph-Traversal, wo das im Alltag wirklich hilft.

11 Min. Lesezeit SplObjectStorage Object Map Graph Traversal spl_object_id

1. Warum ein Array keine Objekte als Schlüssel erlaubt

Der Versuch, ein Objekt direkt als Array-Schlüssel zu verwenden, endet in PHP mit einem TypeError, denn array_key akzeptiert ausschließlich Integer und String. Ein naiver Ausweg ist, spl_object_hash() oder die neuere Funktion spl_object_id() aufzurufen und den zurückgegebenen String beziehungsweise Integer als Schlüssel zu nutzen. Das funktioniert technisch, verliert aber die eigentliche Objektreferenz und erfordert eine zweite parallele Struktur, um Schlüssel und Objekt wieder zusammenzuführen.

Genau dieses Problem löst SplObjectStorage: Sie ist eine Map, deren Schlüssel Objekte sind, verglichen über Objektidentität und nicht über Wertgleichheit. Zwei unterschiedliche Instanzen mit identischen Properties gelten als unterschiedliche Schlüssel, was in vielen Fällen genau das gewünschte Verhalten ist, etwa wenn zwei Bestellungen zufällig dieselben Werte haben, aber dennoch unabhängig behandelt werden müssen.


$order1 = new Order('A-100');
$order2 = new Order('A-100'); // identische Werte, andere Instanz

$storage = new SplObjectStorage();
$storage[$order1] = ['status' => 'processing'];
$storage[$order2] = ['status' => 'pending'];

var_dump($storage->count()); // int(2), Identität statt Wertgleichheit zählt

2. Objekt-Metadaten ohne Property-Verschmutzung

Ein häufiger Anwendungsfall ist das Anhängen von Metadaten an Objekte, die selbst keine passende Property dafür haben sollen, etwa weil die Klasse aus einer fremden Bibliothek stammt oder weil die Metadaten nur temporär für einen bestimmten Verarbeitungsschritt relevant sind. Statt die fremde Klasse um ein zusätzliches Feld zu erweitern oder ein separates Array mit Objekt-Hashes als Schlüssel zu pflegen, lässt sich SplObjectStorage als externe, sauber getrennte Metadaten-Schicht nutzen.

Das ist besonders wertvoll bei Value-Objects oder Entities, deren Klassen bewusst schlank gehalten werden sollen. Ein Validierungs-Durchlauf kann etwa Fehlermeldungen pro Objekt in einer SplObjectStorage-Instanz sammeln, ohne dass die validierten Objekte selbst jemals von der Existenz dieser Fehlerliste wissen. Nach Abschluss des Durchlaufs wird die Storage einfach verworfen, ohne Spuren in den ursprünglichen Objekten zu hinterlassen.


final class ValidationContext
{
    private SplObjectStorage $errors;

    public function __construct()
    {
        $this->errors = new SplObjectStorage();
    }

    public function addError(object $entity, string $message): void
    {
        if (!$this->errors->contains($entity)) {
            $this->errors[$entity] = [];
        }
        $this->errors[$entity][] = $message;
    }

    public function hasErrors(object $entity): bool
    {
        return $this->errors->contains($entity) && count($this->errors[$entity]) > 0;
    }
}

3. Graph-Traversal mit Besuchsmarkierung

Ein zweiter klassischer Einsatzbereich ist die Traversierung von Objektgraphen, etwa bei der Auflösung verschachtelter Kategoriebäume mit Querverweisen oder bei der Analyse von Abhängigkeitsgraphen zwischen Services. Ohne Besuchsmarkierung läuft eine rekursive Traversierung bei zyklischen Referenzen in eine Endlosschleife. SplObjectStorage eignet sich hervorragend als Set bereits besuchter Knoten, weil contains() in konstanter Zeit über die Objektidentität prüft, ohne dass die Knotenklasse selbst ein Besuchs-Flag benötigt.

Der entscheidende Vorteil gegenüber einem einfachen Array mit spl_object_id() als Schlüssel liegt darin, dass SplObjectStorage zusätzlich beliebige Daten pro Knoten speichern kann, etwa die Traversierungstiefe oder den kürzesten bisher gefundenen Pfad. Damit lassen sich Besuchsmarkierung und Zusatzdaten in einer einzigen Struktur führen, statt zwei parallele Arrays synchron halten zu müssen.


function traverse(ServiceNode $start): SplObjectStorage
{
    $visited = new SplObjectStorage();
    $queue = [$start];

    while ($queue !== []) {
        $node = array_shift($queue);
        if ($visited->contains($node)) {
            continue; // Zyklus erkannt, Knoten bereits besucht
        }
        $visited[$node] = ['depth' => count($visited)];

        foreach ($node->getDependencies() as $dependency) {
            $queue[] = $dependency;
        }
    }

    return $visited;
}

4. Die API im Detail: attach, detach, contains, offsetSet

SplObjectStorage implementiert sowohl Countable als auch ArrayAccess und Iterator, wodurch sich sowohl die klassische Methoden-API mit attach() und detach() als auch die kompaktere Array-Syntax mit eckigen Klammern verwenden lässt. attach($object, $data) fügt ein Objekt mit optionalen Zusatzdaten hinzu, $storage[$object] = $data tut funktional dasselbe. Beide Schreibweisen sind gleichwertig, in der Praxis hat sich die Array-Syntax für Lesbarkeit weitgehend durchgesetzt.

Bei der Iteration mit foreach liefert die Storage standardmäßig die Objekte selbst, die Zusatzdaten werden über getInfo() innerhalb der Schleife abgerufen, nicht über den zweiten Wert eines Key-Value-Paares wie bei einem Array. Das ist ein häufiger Stolperstein für Entwickler, die von der Array-Iteration kommen und zunächst foreach ($storage as $obj => $data) erwarten, was syntaktisch zwar funktioniert, aber semantisch etwas anderes bedeutet als gedacht.


foreach ($visited as $node) {
    $data = $visited->getInfo(); // Zusatzdaten des aktuellen Objekts
    echo $node->getName() . ' Tiefe: ' . $data['depth'] . PHP_EOL;
}

// Aequivalent mit expliziter Methoden-API
$visited->attach($newNode, ['depth' => 3]);
if ($visited->contains($newNode)) {
    $visited->detach($newNode);
}

5. Performance-Charakteristik: attach, contains und Iteration

Intern verwaltet SplObjectStorage ihre Einträge über den internen Objekt-Handle, vergleichbar mit dem Wert, den auch spl_object_id() zurückgibt. Dadurch laufen attach(), contains() und detach() im Durchschnitt in konstanter Zeit ab, unabhängig von der Anzahl der bereits gespeicherten Objekte. Das macht die Klasse auch für Graphtraversierungen mit mehreren tausend Knoten praktikabel, ohne dass Lookups mit wachsender Besuchsmenge spürbar langsamer werden.

Bei der Iterationsreihenfolge garantiert SplObjectStorage die Reihenfolge des Einfügens, ähnlich wie ein PHP-Array seine Insertion-Order beibehält. Das ist relevant, wenn etwa Validierungsfehler in der Reihenfolge ihres Auftretens ausgegeben werden sollen. Ein wichtiger Unterschied zu einem Array bleibt jedoch, dass ein Entfernen mit detach() keine Neuindizierung auslöst, weil es ohnehin keine numerischen Indizes gibt, auf die sich der Rest der Struktur verlassen müsste.

6. Vergleich zu WeakMap: Referenzhaltung als Kernunterschied

Der wichtigste Unterschied zwischen SplObjectStorage und WeakMap betrifft die Referenzhaltung. SplObjectStorage hält eine starke Referenz auf jedes gespeicherte Objekt, wodurch der Garbage Collector das Objekt nicht freigeben kann, solange es in der Storage liegt. Das ist bei Metadaten mit klarem, begrenztem Lebenszyklus unproblematisch, etwa innerhalb eines einzelnen Requests oder eines abgeschlossenen Validierungslaufs, kann aber bei langlebigen Caches zu Speicherlecks führen, weil Objekte künstlich am Leben gehalten werden.

WeakMap dagegen hält nur schwache Referenzen, sodass ein Objekt trotzdem freigegeben wird, sobald keine weitere starke Referenz mehr existiert, und automatisch aus der Map verschwindet. Für kurzlebige, explizit verwaltete Strukturen wie den oben gezeigten Traversierungs-Besuchssatz ist SplObjectStorage die einfachere und schnellere Wahl, weil sie keinen zusätzlichen Aufwand für schwache Referenzen betreiben muss. Für langlebige, prozessweite Caches ist WeakMap in der Regel die sicherere Option.

7. Mengenoperationen: addAll, removeAll und Set-Semantik

Ein weniger bekanntes Feature von SplObjectStorage sind die Methoden addAll() und removeAll(), die eine ganze andere Storage-Instanz übernehmen beziehungsweise entfernen. Damit lassen sich klassische Mengenoperationen ausdrücken: Die Vereinigung zweier Besuchsmengen entsteht durch addAll() auf einer Kopie der ersten Menge, während removeAll() effektiv eine Differenzmenge bildet. Für einen echten Schnittmengen-Vergleich gibt es keine eingebaute Methode, hier hilft eine einfache Filterung über contains().

Diese Mengenoperationen sind besonders bei Graphalgorithmen relevant, etwa wenn zwei unabhängig ermittelte Besuchsmengen aus einer bidirektionalen Suche zusammengeführt werden müssen. Da SplObjectStorage intern Duplikate über Objektidentität automatisch ausschließt, verhält sich addAll() korrekt wie eine Mengenvereinigung, ohne dass Entwickler manuell auf bereits vorhandene Objekte prüfen müssen.


$union = new SplObjectStorage();
$union->addAll($visitedFromA);
$union->addAll($visitedFromB); // Duplikate werden automatisch ignoriert

$onlyInA = new SplObjectStorage();
$onlyInA->addAll($visitedFromA);
$onlyInA->removeAll($visitedFromB); // Differenzmenge A ohne B

8. Serialisierung und Grenzen bei Persistenz

SplObjectStorage lässt sich grundsätzlich serialisieren, da die Klasse Serializable-ähnliches Verhalten über Magic Methods implementiert. In der Praxis ist davon jedoch eher abzuraten, weil die serialisierte Form die Objektidentität nicht über Prozessgrenzen hinweg bewahrt: Nach der Deserialisierung entstehen neue Objektinstanzen mit neuen Identitäten, wodurch Vergleiche mit ursprünglich referenzierten Objekten fehlschlagen. Für Anwendungsfälle, die eine echte Persistenz benötigen, ist eine eigene Serialisierungslogik auf Basis stabiler Identifikatoren wie IDs meist die robustere Lösung.

Ein weiterer praktischer Hinweis betrifft die JSON-Serialisierung: SplObjectStorage implementiert standardmäßig kein sinnvolles JsonSerializable-Verhalten, ein direkter json_encode()-Aufruf liefert meist nur ein leeres Objekt. Wer die Inhalte als JSON exportieren will, sollte die Storage explizit in ein assoziatives Array transformieren, üblicherweise über einen stabilen Bezeichner des jeweiligen Objekts als Ersatzschlüssel.

9. Wann sich SplObjectStorage im Alltag wirklich lohnt

SplObjectStorage lohnt sich immer dann, wenn Objekte als eindeutige, identitätsbasierte Schlüssel benötigt werden und die zugehörigen Daten weder in der Objektklasse selbst noch in einer parallel gepflegten Hilfsstruktur untergebracht werden sollen. Typische Kandidaten sind Validierungs-Kontexte, Besuchsmengen bei Graphtraversierungen, Event-Listener-Register mit Objektbezug oder temporäre Berechnungs-Caches innerhalb eines einzelnen Requests.

Weniger sinnvoll ist der Einsatz, wenn die Lebensdauer der gespeicherten Objekte den Lebenszyklus der Storage deutlich überdauern soll, hier ist WeakMap fast immer die bessere Wahl, um Speicherlecks zu vermeiden. Ebenso wenig lohnt sich SplObjectStorage, wenn ohnehin nur String- oder Integer-Schlüssel benötigt werden, weil dann ein gewöhnliches Array sowohl einfacher als auch ohne zusätzlichen Objekt-Overhead auskommt.

Merkmal SplObjectStorage Array mit spl_object_id WeakMap
Objekte als Schlüssel Direkt unterstützt Nur über Umweg (String/Int) Direkt unterstützt
Referenzhaltung Stark, verhindert GC Stark, indirekt Schwach, GC-freundlich
Zusatzdaten pro Objekt Ja, über getInfo/attach Zweites paralleles Array nötig Ja, als Value
Mengenoperationen addAll, removeAll eingebaut Manuell nachbauen Nicht eingebaut
Typischer Einsatz Kurzlebige Metadaten, Graphen Historisch, meist ersetzbar Langlebige Caches

Mironsoft

PHP-Modernisierung, Code-Qualität und Legacy-Refactoring

Gewachsener PHP-Code, der niemand mehr gern anfasst?

Wir modernisieren PHP-Codebasen auf aktuelle Sprachstandards, führen statische Analyse und Coding Standards ein und refactorn Legacy-Code Schritt für Schritt, ohne den laufenden Betrieb zu gefährden.

Legacy-Refactoring

Gewachsenen PHP-Code strukturiert und risikoarm modernisieren.

Code-Qualität etablieren

PHPStan, Coding Standards und CI-Checks nachhaltig im Team verankern.

Versions-Upgrade

PHP-Major-Version-Upgrades sicher planen und ohne Ausfallzeit umsetzen.

10. Zusammenfassung

SplObjectStorage: Das Wichtigste auf einen Blick

Problem

Arrays akzeptieren nur Integer und String als Schlüssel, keine Objekte.

Lösung

SplObjectStorage nutzt Objektidentität als Schlüssel und speichert Zusatzdaten getrennt.

Praxis

Ideal für Validierungs-Metadaten und Besuchsmengen bei Graph-Traversal.

Abgrenzung

Für langlebige Caches ohne Speicherlecks ist WeakMap wegen schwacher Referenzen vorzuziehen.

11. FAQ: SplObjectStorage: Das Wichtigste auf einen Blick

1Warum kann ein PHP-Array kein Objekt als Schlüssel verwenden?
Array-Schlüssel akzeptieren in PHP ausschließlich Integer und String. Ein Objekt als Schlüssel führt zu einem TypeError, weil die Sprache keine implizite Konvertierung dafür vorsieht.
2Wie unterscheidet SplObjectStorage zwischen zwei Objekten?
Über Objektidentität, nicht über Wertgleichheit. Zwei Instanzen mit identischen Properties gelten als unterschiedliche Schlüssel, solange es sich um unterschiedliche Objekte im Speicher handelt.
3Wie greife ich innerhalb einer foreach-Schleife auf Zusatzdaten zu?
Über die Methode getInfo() während der Iteration, nicht über den zweiten Wert eines Key-Value-Paares wie bei einem normalen Array.
4Was ist der Hauptunterschied zwischen SplObjectStorage und WeakMap?
SplObjectStorage hält starke Referenzen und verhindert damit die Garbage Collection der gespeicherten Objekte, WeakMap hält schwache Referenzen und lässt Objekte automatisch verschwinden, sobald sie sonst nirgends mehr referenziert werden.
5Eignet sich SplObjectStorage für langlebige Caches?
Eher nicht, weil starke Referenzen die gespeicherten Objekte künstlich am Leben halten. Für langlebige, prozessweite Caches ist WeakMap in der Regel die sicherere Wahl.
6Wie verhindere ich Endlosschleifen bei zyklischen Objektgraphen?
Indem bereits besuchte Knoten in einer SplObjectStorage als Set markiert werden. contains() prüft die Objektidentität in konstanter Zeit, ohne dass die Knotenklasse ein eigenes Besuchs-Flag benötigt.
7Gibt es eingebaute Mengenoperationen bei SplObjectStorage?
Ja, addAll fügt alle Elemente einer anderen Storage hinzu und bildet damit eine Vereinigungsmenge, removeAll entfernt sie und bildet damit eine Differenzmenge. Eine echte Schnittmenge muss manuell über contains gefiltert werden.
8Kann ich SplObjectStorage einfach als JSON exportieren?
Nicht direkt, json_encode liefert meist ein leeres Objekt. Für den Export sollte die Storage explizit in ein assoziatives Array mit einem stabilen Objekt-Bezeichner als Ersatzschlüssel transformiert werden.
9Bleibt die Objektidentität nach einer Serialisierung erhalten?
Nein. Nach der Deserialisierung entstehen neue Objektinstanzen mit neuen Identitäten, wodurch Vergleiche mit den ursprünglich referenzierten Objekten fehlschlagen.
10Wann ist ein einfaches Array trotzdem die bessere Wahl?
Wenn ohnehin nur String- oder Integer-Schlüssel benötigt werden. Dann kommt ein gewöhnliches Array ohne den zusätzlichen Objekt-Overhead von SplObjectStorage aus.