Was ist once()?
once() ist ein globaler Helper, der einen Callback ausführt und dessen Ergebnis für die Dauer des Requests im Speicher cacht. Wird derselbe Callback erneut von derselben Aufrufstelle heraus aufgerufen, gibt der Helper das zwischengespeicherte Ergebnis zurück.
Der Mechanismus, dass der Cache „pro Aufrufstelle und pro Instanz” isoliert wird, unterscheidet sich von einfachen Memoization-Helpern wie
memoize. Um ihn zu verstehen, muss man sich die beiden Klassen Illuminate\Support\Once und Illuminate\Support\Onceable anschauen.Der Aufruf in helpers.php
Der Rumpf der Funktiononce() in Illuminate\Support\helpers.php ist denkbar einfach.
once() selbst hält keinen Cache. Es baut aus den Informationen zum Aufrufer, die debug_backtrace() liefert, eine Onceable-Instanz zusammen und delegiert die eigentliche Verarbeitung an die Klasse Once.
Onceable — Hash-Berechnung zur Identifikation der Aufrufstelle
Die KlasseOnceable ist dafür zuständig, aus dem Backtrace einen Hash zu berechnen, der eindeutig festlegt, „von welcher Aufrufstelle” der Aufruf kommt.
Dateipfad + Klassenname + Funktionsname + Zeilennummer + Werte der von der Closure per use eingefangenen Variablen. Anders gesagt: Selbst wenn once() aus derselben Zeile aufgerufen wird, gilt es als anderer Cache, sobald sich der Wert einer per use eingefangenen Variablen ändert.
object gibt an, ob der Aufrufer eine Instanzmethode ist. $trace[1]['object'] bezieht sich auf eine Ebene über once() (also die Seite, die once() aufgerufen hat). Bei einer Instanzmethode landet dort $this; bei einer statischen Methode oder einer globalen Funktion ist der Wert null.
Once — der eigentliche Cache über WeakMap
Die tatsächliche Haltung des Caches übernimmtIlluminate\Support\Once.
WeakMap. Als Schlüssel dient $onceable->object (die Aufrufer-Instanz); der Wert ist ein assoziatives Array von Hash → Ergebnis. Fehlt object (bei globalen Funktionen oder statischen Methoden), dient $this von Once selbst als Schlüssel – das bedeutet praktisch einen einzigen, prozessweit geteilten Cache-Bereich.
Da
WeakMap verwendet wird, wird auch der an das Objekt gebundene Cache Garbage-Collection-fähig, sobald es keine weiteren Referenzen mehr auf das Schlüssel-Objekt gibt. Selbst wenn Sie once() aus vielen Objekten heraus aufrufen, ist das Design robust gegen Memory-Leaks.Einsatz in Tests — enable / disable / flush
Ein Aufruf vonOnce::disable() deaktiviert den Cache vollständig; once() führt den Callback dann jedes Mal aus. Praktisch für Tests, in denen Sie „jedes Mal einen neuen Wert” möchten.
Once::flush() verwirft den gesamten Cache und legt beim nächsten Zugriff einen neuen WeakMap an. Sinnvoll bei Tests für Artisan-Befehle oder in Umgebungen, in denen Prozesse wiederverwendet werden (z. B. Octane), wenn Sie den Cache nicht zwischen Requests behalten möchten.
PreventsCircularRecursion — ein Anwendungsbeispiel von Onceable
Onceable::tryFromTrace() wird nicht nur von once() genutzt. Auch das Eloquent-Trait Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion verwendet es wieder. Dieses Trait dient dazu, „denselben Aufruf desselben Objekts innerhalb des Call-Stacks nicht erneut zuzulassen”.
Once liegt darin, dass der Cache im finally-Block nach Abschluss des Aufrufs wieder gelöscht wird. Während Once „während des Requests dauerhaft cacht”, nutzt PreventsCircularRecursion denselben Onceable-Mechanismus, um „nur für die Dauer des aktuellen Call-Stacks zu cachen (praktisch als Lock)”.
Ein typischer Einsatz bei Eloquent-Modellen ist die Vermeidung von Rekursionen, wenn toArray() oder ein Accessor sich selbst rekursiv referenziert.
Einsatz in der Paketentwicklung
Möchten Sie in einem eigenen Paket „denselben Methodenaufruf derselben Instanz während eines Requests nur einmal ausführen”, ist es am einfachsten,once() direkt zu verwenden. Direkt mit Onceable/Once zu arbeiten kommt seltener vor, doch in folgenden Situationen hilft das Verständnis der internen Implementierung:
- Wenn Sie in Artisan-Befehlen oder Tests mit
Once::disable()/Once::flush()das Cache-Verhalten steuern möchten - Wenn Sie in einem eigenen Trait einen Cache oder eine Rekursionssperre „pro Aufrufstelle” implementieren möchten (dasselbe Muster wie
PreventsCircularRecursion) - Wenn das Ergebnis von
once()von der Erwartung abweicht und Sie debuggen müssen: Ein Verständnis der Bestandteile, die in den Hash einfließen (Datei, Klasse, Funktion, Zeile, perusegefangene Variablen), erleichtert die Ursachensuche
Verwandte Seiten
tap()-Helper und Tappable-Trait
Das Implementierungsmuster des tap()-Helpers, der einen Seiteneffekt einschleift und dennoch den Wert zurückgibt.
Eloquent-Observer und Model-Events
Zentrale Verwaltung über Events und Observer von Eloquent-Modellen.