paint-brush
Laravel unter der Haube – Was sind Fassaden?von@oussamamater
Neue Geschichte

Laravel unter der Haube – Was sind Fassaden?

von Oussama Mater14m2024/06/29
Read on Terminal Reader

Zu lang; Lesen

Laravel wird mit vielen Fassaden ausgeliefert, die wir häufig verwenden. Wir besprechen, was sie sind, wie wir unsere eigenen Fassaden erstellen können und lernen auch etwas über Echtzeitfassaden.
featured image - Laravel unter der Haube – Was sind Fassaden?
Oussama Mater HackerNoon profile picture

Sie haben gerade eine neue Laravel-Anwendung installiert, sie gestartet und die Willkommensseite erhalten. Wie alle anderen möchten Sie sehen, wie sie gerendert wird. Sie springen also in die Datei web.php und stoßen auf diesen Code:

 <?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); });

Es ist offensichtlich, wie wir die Willkommensansicht erhalten haben, aber Sie sind neugierig, wie der Router von Laravel funktioniert, also beschließen Sie, in den Code einzutauchen. Die anfängliche Annahme ist: Es gibt eine Route Klasse, für die wir eine statische Methode get() aufrufen. Wenn Sie jedoch darauf klicken, gibt es dort keine get() -Methode. Was für eine Art schwarzer Magie passiert also hier? Lassen Sie uns das entmystifizieren!

Normale Fassaden

Bitte beachten Sie, dass ich der Einfachheit halber die meisten PHPDocs entfernt und die Typen integriert habe. „…“ verweist auf weiteren Code.

Ich empfehle dringend, Ihre IDE zu öffnen und dem Code zu folgen, um Verwirrung zu vermeiden.


Lassen Sie uns unserem Beispiel folgen und die Route Klasse untersuchen.

 <?php namespace Illuminate\Support\Facades; class Route extends Facade { // ... protected static function getFacadeAccessor(): string { return 'router'; } }


Hier gibt es nicht viel, nur die Methode getFacadeAccessor() , die den String router zurückgibt. Unter Berücksichtigung dessen gehen wir nun zur übergeordneten Klasse über.

 <?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }

Innerhalb der übergeordneten Klasse gibt es viele Methoden, aber keine get() Methode. Aber es gibt eine interessante, die __callStatic() Methode. Es ist eine magische Methode, die aufgerufen wird, wenn eine undefinierte statische Methode, wie in unserem Fall get() , aufgerufen wird. Daher stellt unser Aufruf __callStatic('get', ['/', Closure()]) dar, was wir beim Aufruf Route::get() übergeben haben, die Route / und ein Closure() , das die Willkommensansicht zurückgibt.


Wenn __callStatic() ausgelöst wird, versucht es zuerst, eine Variable $instance durch Aufruf von getFacadeRoot() zu setzen. Die $instance enthält die eigentliche Klasse, an die der Aufruf weitergeleitet werden soll. Schauen wir uns das genauer an, es wird gleich Sinn ergeben

 // Facade.php public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); }


Hey, schau mal, es ist getFacadeAccessor() von der untergeordneten Klasse Route , von der wir wissen, dass sie den String router zurückgegeben hat. Dieser router String wird dann an resolveFacadeInstance() übergeben, das versucht, ihn in eine Klasse aufzulösen, eine Art Zuordnung, die besagt: „Welche Klasse repräsentiert dieser String?“ Mal sehen.

 // Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }

Zuerst wird geprüft, ob ein statisches Array, $resolvedInstance , einen Wert mit dem angegebenen $name (der wiederum router ist) hat. Wenn eine Übereinstimmung gefunden wird, wird einfach dieser Wert zurückgegeben. Dies ist Laravel-Caching, um die Leistung ein wenig zu optimieren. Dieses Caching erfolgt innerhalb einer einzelnen Anfrage. Wenn diese Methode innerhalb derselben Anfrage mehrmals mit demselben Argument aufgerufen wird, verwendet sie den zwischengespeicherten Wert. Nehmen wir an, es handelt sich um den ersten Aufruf und fahren fort.


Anschließend wird geprüft, ob $app gesetzt ist und $app eine Instanz des Anwendungscontainers ist

 // Facade.php protected static \Illuminate\Contracts\Foundation\Application $app;

Wenn Sie wissen möchten, was ein Anwendungscontainer ist, stellen Sie sich ihn als eine Box vor, in der Ihre Klassen gespeichert sind. Wenn Sie diese Klassen benötigen, greifen Sie einfach in diese Box. Manchmal vollbringt dieser Container ein bisschen Magie. Selbst wenn die Box leer ist und Sie nach einer Klasse greifen, wird sie für Sie bereitgestellt. Das ist ein Thema für einen anderen Artikel.


Jetzt fragen Sie sich vielleicht: „Wann wird $app festgelegt?“, denn das muss sein, sonst haben wir unsere $instance nicht. Dieser Anwendungscontainer wird während des Bootstrapping-Prozesses unserer Anwendung festgelegt. Werfen wir einen kurzen Blick auf die Klasse \Illuminate\Foundation\Http\Kernel :

 <?php namespace Illuminate\Foundation\Http; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Facade; use Illuminate\Contracts\Http\Kernel as KernelContract; // ... class Kernel implements KernelContract { // ... protected $app; protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Illuminate\Foundation\Bootstrap\HandleExceptions::class, \Illuminate\Foundation\Bootstrap\RegisterFacades::class, // <- this guy \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; public function bootstrap(): void { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } }

Wenn eine Anfrage eingeht, wird sie an den Router gesendet. Kurz davor wird die Methode bootstrap() aufgerufen, die das bootstrappers Array verwendet, um die Anwendung vorzubereiten. Wenn Sie die Methode bootstrapWith() in der Klasse \Illuminate\Foundation\Application untersuchen, iteriert sie durch diese Bootstrapper und ruft deren Methode bootstrap() auf.


Der Einfachheit halber konzentrieren wir uns nur auf \Illuminate\Foundation\Bootstrap\RegisterFacades , von dem wir wissen, dass es eine bootstrap() Methode enthält, die in bootstrapWith() aufgerufen wird.

 <?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\PackageManifest; use Illuminate\Support\Facades\Facade; class RegisterFacades { // ... public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); // Interested here AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); } }


Und da ist es, wir legen den Anwendungscontainer für die Facade Klasse mithilfe der statischen Methode setFacadeApplication().

 // RegisterFacades.php public static function setFacadeApplication($app) { static::$app = $app; }


Sehen Sie, wir weisen die $app Eigenschaft zu, die wir innerhalb von resolveFacadeInstance() testen. Dies beantwortet die Frage; fahren wir fort.

 // Facade.php protected static function resolveFacadeInstance($name) { if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } if (static::$app) { if (static::$cached) { return static::$resolvedInstance[$name] = static::$app[$name]; } return static::$app[$name]; } }

Wir haben bestätigt, dass $app während des Anwendungs-Bootstrappings festgelegt wurde. Der nächste Schritt besteht darin, zu prüfen, ob die aufgelöste Instanz zwischengespeichert werden soll, indem $cached überprüft wird, das standardmäßig auf true gesetzt ist. Schließlich rufen wir die Instanz aus dem Anwendungscontainer ab. In unserem Fall ist dies so, als würden wir static::$app['router'] bitten, eine beliebige Klasse bereitzustellen, die an die Zeichenfolge router gebunden ist.


Nun fragen Sie sich vielleicht, warum wir $app wie auf ein Array zugreifen, obwohl es eine Instanz des Anwendungscontainers und damit ein Objekt ist. Sie haben Recht! Der Anwendungscontainer implementiert jedoch eine PHP-Schnittstelle namens ArrayAccess , die arrayähnlichen Zugriff ermöglicht. Wir können uns das einmal genauer ansehen, um dies zu bestätigen:

 <?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... }


Die resolveFacadeInstance() gibt also tatsächlich eine Instanz zurück, die an den router String gebunden ist, nämlich \Illuminate\Routing\Router . Woher ich das wusste? Sehen Sie sich die Route Fassade an. Oft finden Sie ein PHPDoc @see , das darauf hinweist, was diese Fassade verbirgt oder genauer gesagt, an welche Klasse unsere Methodenaufrufe weitergeleitet werden.


Nun zurück zu unserer __callStatic -Methode.

 <?php namespace Illuminate\Support\Facades; use RuntimeException; // ... abstract class Facade { // ... public static function __callStatic(string $method, array $args): mixed { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }


Wir haben $instance , ein Objekt der Klasse \Illuminate\Routing\Router . Wir testen, ob es gesetzt ist (was in unserem Fall bestätigt wird) und rufen die Methode direkt darauf auf. Das Ergebnis lautet also:

 // Facade.php return $instance->get('/', Closure());


Und jetzt können Sie bestätigen, dass get() in der Klasse \Illuminate\Routing\Router vorhanden ist.

 <?php namespace Illuminate\Routing; use Illuminate\Routing\Route; use Illuminate\Contracts\Routing\BindingRegistrar; use Illuminate\Contracts\Routing\Registrar as RegistrarContract; // ... class Router implements BindingRegistrar, RegistrarContract { // ... public function get(string $uri, array|string|callable|null $action = null): Route { return $this->addRoute(['GET', 'HEAD'], $uri, $action); } }

Damit ist es erledigt! War das am Ende nicht schwierig? Um es noch einmal zusammenzufassen: Eine Fassade gibt einen String zurück, der an den Container gebunden ist. Beispielsweise könnte hello-world an die Klasse HelloWorld gebunden sein. Wenn wir eine undefinierte Methode auf einer Fassade statisch aufrufen, beispielsweise HelloWorldFacade , greift __callStatic() ein.


Es löst den in seiner getFacadeAccessor() -Methode registrierten String in das auf, was im Container gebunden ist, und leitet unseren Aufruf an die abgerufene Instanz weiter. Somit erhalten wir (new HelloWorld())->method() . Das ist das Wesentliche! Hat es bei Ihnen immer noch nicht geklickt? Dann erstellen wir unsere Fassade!

Lasst uns unsere Fassade gestalten

Angenommen, wir haben diese Klasse:

 <?php namespace App\Http\Controllers; class HelloWorld { public function greet(): string { return "Hello, World!"; } }


Das Ziel besteht darin HelloWorld::greet() aufzurufen. Dazu binden wir unsere Klasse an den Anwendungscontainer. Navigieren Sie zunächst zu AppServiceProvider .

 <?php namespace App\Providers; use App\Http\Controllers; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind('hello-world', function ($app) { return new HelloWorld; }); } // ... }


Wenn wir jetzt hello-world von unserem Anwendungscontainer (oder der Box, wie ich bereits erwähnt habe) anfordern, gibt dieser eine Instanz von HelloWorld “ zurück. Was bleibt übrig? Erstellen Sie einfach eine Fassade, die die Zeichenfolge hello-world zurückgibt.

 <?php namespace App\Http\Facades; use Illuminate\Support\Facades\Facade; class HelloWorldFacade extends Facade { protected static function getFacadeAccessor() { return 'hello-world'; } }


Wenn dies vorhanden ist, können wir es verwenden. Rufen wir es in unserer web.php.

 <?php use App\Http\Facades; use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorldFacade::greet(); // Hello, World! });


Wir wissen, dass greet() auf der HelloWorldFacade Fassade nicht existiert, __callStatic() wird ausgelöst. Es zieht eine durch einen String dargestellte Klasse (in unserem Fall hello-world ) aus dem Anwendungscontainer. Und wir haben diese Bindung bereits im AppServiceProvider vorgenommen; wir haben ihn angewiesen, eine Instanz von HelloWorld bereitzustellen, wenn jemand hello-world anfordert. Folglich wird jeder Aufruf, wie beispielsweise greet() , auf diese abgerufene Instanz von HelloWorld angewendet. Und das war’s.


Herzlichen Glückwunsch! Sie haben Ihre ganz persönliche Fassade erstellt!

Laravel Echtzeitfassaden

Nachdem Sie nun ein gutes Verständnis von Fassaden haben, können wir Ihnen noch einen weiteren Zaubertrick enthüllen. Stellen Sie sich vor, Sie könnten HelloWorld::greet() aufrufen, ohne eine Fassade zu erstellen, und zwar mithilfe von Echtzeit-Fassaden .


Werfen wir einen Blick:

 <?php use Facades\App\Http\Controllers; // Notice the prefix use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorld::greet(); // Hello, World! });


Indem wir dem Namespace des Controllers Facades voranstellen, erzielen wir dasselbe Ergebnis wie zuvor. Es ist jedoch sicher, dass der HelloWorld Controller keine statische Methode mit dem Namen greet() hat! Und woher kommt Facades\App\Http\Controllers\HelloWorld überhaupt? Ich verstehe, dass dies wie Zauberei erscheinen mag, aber wenn man es einmal verstanden hat, ist es ganz einfach.


Schauen wir uns die \Illuminate\Foundation\Bootstrap\RegisterFacades genauer an, die wir zuvor überprüft haben. Es handelt sich um die Klasse, die für die Festlegung von $app:

 <?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\PackageManifest; use Illuminate\Support\Facades\Facade; class RegisterFacades { public function bootstrap(Application $app): void { Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); AliasLoader::getInstance(array_merge( $app->make('config')->get('app.aliases', []), $app->make(PackageManifest::class)->aliases() ))->register(); // Interested here } }


Ganz am Ende sieht man, dass die Methode register() aufgerufen wird. Werfen wir einen Blick hinein:

 <?php namespace Illuminate\Foundation; class AliasLoader { // ... protected $registered = false; public function register(): void { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } }


Die Variable $registered ist zunächst auf false gesetzt. Daher geben wir die if Anweisung ein und rufen die Methode prependToLoaderStack() auf. Sehen wir uns nun ihre Implementierung an.

 // AliasLoader.php protected function prependToLoaderStack(): void { spl_autoload_register([$this, 'load'], true, true); }


Hier geschieht die Magie! Laravel ruft die Funktion spl_autoload_register() auf, eine integrierte PHP-Funktion, die ausgelöst wird, wenn versucht wird, auf eine undefinierte Klasse zuzugreifen. Sie definiert die Logik, die in solchen Situationen ausgeführt werden soll. In diesem Fall ruft Laravel die Methode load() auf, wenn ein undefinierter Aufruf auftritt.


Darüber hinaus übergibt spl_autoload_register() den Namen der nicht definierten Klasse automatisch an die Methode oder Funktion, die es aufruft.


Lassen Sie uns die load() Methode untersuchen. Sie muss der Schlüssel sein.

 // AliasLoader.php public function load($alias) { if (static::$facadeNamespace && str_starts_with($alias, static::$facadeNamespace)) { $this->loadFacade($alias); return true; } if (isset($this->aliases[$alias])) { return class_alias($this->aliases[$alias], $alias); } }

Wir prüfen, ob $facadeNamespace festgelegt ist und ob die übergebene Klasse, in unserem Fall Facades\App\Http\Controllers\HelloWorld mit dem beginnt, was in $facadeNamespace festgelegt ist.


Die Logik prüft, ob $facadeNamespace festgelegt ist und ob die übergebene Klasse, in unserem Fall Facades\App\Http\Controllers\HelloWorld (die nicht definiert ist), mit dem in $facadeNamespace.

 // AliasLoader.php protected static $facadeNamespace = 'Facades\\';


Da wir dem Namespace unseres Controllers Facades vorangestellt haben und damit die Bedingung erfüllen, fahren wir mit loadFacade() fort.

 // AliasLoader.php protected function loadFacade($alias) { require $this->ensureFacadeExists($alias); }


Hier erfordert die Methode den Pfad, der von ensureFacadeExists() zurückgegeben wird. Der nächste Schritt besteht also darin, sich mit der Implementierung zu befassen.

 // AliasLoader.php protected function ensureFacadeExists($alias) { if (is_file($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) { return $path; } file_put_contents($path, $this->formatFacadeStub( $alias, file_get_contents(__DIR__.'/stubs/facade.stub') )); return $path; }


Zunächst wird geprüft, ob eine Datei namens framework/cache/facade-'.sha1($alias).'.php' existiert. In unserem Fall ist diese Datei nicht vorhanden, was den nächsten Schritt auslöst: file_put_contents() . Diese Funktion erstellt eine Datei und speichert sie im angegebenen $path . Der Inhalt der Datei wird von formatFacadeStub() generiert, das, dem Namen nach zu urteilen, eine Fassade aus einem Stub erstellt. Wenn Sie facade.stub untersuchen würden, würden Sie Folgendes finden:

 <?php namespace DummyNamespace; use Illuminate\Support\Facades\Facade; /** * @see \DummyTarget */ class DummyClass extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'DummyTarget'; } }


Kommt Ihnen das bekannt vor? Das ist im Wesentlichen das, was wir manuell gemacht haben. Nun ersetzt formatFacadeStub() den Dummy-Inhalt durch unsere undefinierte Klasse, nachdem das Präfix Facades\\ entfernt wurde. Diese aktualisierte Fassade wird dann gespeichert. Wenn loadFacade() die Datei benötigt, tut es dies daher korrekt und benötigt am Ende die folgende Datei:

 <?php namespace Facades\App\Http\Controllers; use Illuminate\Support\Facades\Facade; /** * @see \App\Http\Controllers\HelloWorld */ class HelloWorld extends Facade { /** * Get the registered name of the component. */ protected static function getFacadeAccessor(): string { return 'App\Http\Controllers\HelloWorld'; } }

Und jetzt bitten wir im üblichen Ablauf den Anwendungscontainer, jede Instanz zurückzugeben, die an die Zeichenfolge App\Http\Controllers\HelloWorld gebunden ist. Sie fragen sich vielleicht, wir haben diese Zeichenfolge an nichts gebunden, wir haben unseren AppServiceProvider nicht einmal berührt. Aber erinnern Sie sich, was ich ganz am Anfang über den Anwendungscontainer gesagt habe?


Auch wenn das Feld leer ist, wird die Instanz zurückgegeben , allerdings unter einer Bedingung: Die Klasse darf keinen Konstruktor haben. Andernfalls wüsste sie nicht, wie sie diese für Sie erstellen soll. In unserem Fall benötigt unsere HelloWorld Klasse keine Argumente, um erstellt zu werden. Der Container löst sie also auf, gibt sie zurück und alle Aufrufe werden an sie weitergeleitet.


Zusammenfassung zu Echtzeit-Fassaden: Wir haben unserer Klasse Facades vorangestellt. Während des Anwendungs-Bootstrappings registriert Laravel spl_autoload_register() , das ausgelöst wird, wenn wir undefinierte Klassen aufrufen. Es führt schließlich zur Methode load() . Innerhalb von load() prüfen wir, ob die aktuelle undefinierte Klasse Facades vorangestellt hat. Das stimmt überein, also versucht Laravel, sie zu laden.


Da die Fassade nicht existiert, wird sie aus einem Stub erstellt und dann die Datei angefordert. Und voilà! Sie haben eine normale Fassade, aber diese wurde spontan erstellt. Ziemlich cool, oder?

Abschluss

Herzlichen Glückwunsch, dass Sie es so weit geschafft haben! Ich verstehe, dass es ein wenig überwältigend sein kann. Gehen Sie ruhig zurück und lesen Sie alle Abschnitte, die bei Ihnen nicht ganz gezündet haben, noch einmal durch. Auch die Nachverfolgung Ihrer IDE kann hilfreich sein. Aber hey, keine schwarze Magie mehr, muss sich gut anfühlen, zumindest habe ich mich beim ersten Mal so gefühlt!


Und denken Sie daran, dass dies beim nächsten statischen Aufruf einer Methode möglicherweise nicht der Fall ist 🪄