paint-brush
Laravel Under The Hood – Que sont les façades ?par@oussamamater
Nouvelle histoire

Laravel Under The Hood – Que sont les façades ?

par Oussama Mater14m2024/06/29
Read on Terminal Reader

Trop long; Pour lire

Laravel est livré avec de nombreuses façades que nous utilisons souvent. Nous discuterons de ce qu'elles sont, de la manière dont nous pouvons créer nos propres façades, et nous en apprendrons également davantage sur les façades en temps réel.
featured image - Laravel Under The Hood – Que sont les façades ?
Oussama Mater HackerNoon profile picture

Vous venez d'installer une nouvelle application Laravel, de la démarrer et d'obtenir la page d'accueil. Comme tout le monde, vous essayez de voir comment il est rendu, alors vous sautez dans le fichier web.php et rencontrez ce code :

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

Il est évident que nous avons obtenu la vue de bienvenue, mais vous êtes curieux de savoir comment fonctionne le routeur de Laravel, alors vous décidez de vous plonger dans le code. L'hypothèse initiale est la suivante : il existe une classe Route sur laquelle nous appelons une méthode statique get() . Cependant, en cliquant dessus, il n’y a pas de méthode get() . Alors, quel genre de magie noire se produit ? Démystifions cela !

Façades régulières

Veuillez noter que j'ai supprimé la plupart des PHPDocs et intégré les types juste pour plus de simplicité, "..." fait référence à plus de code.

Je suggère fortement d'ouvrir votre IDE et de suivre le code pour éviter toute confusion.


En suivant notre exemple, explorons la classe Route .

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


Il n'y a pas grand chose ici, juste la méthode getFacadeAccessor() qui renvoie la chaîne router . En gardant cela à l’esprit, passons à la classe parent.

 <?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); } }

Dans la classe parent, il existe de nombreuses méthodes, mais il n’existe pas de méthode get() . Mais il en existe une intéressante, la méthode __callStatic() . C'est une méthode magique , invoquée chaque fois qu'une méthode statique non définie, comme get() dans notre cas, est appelée. Par conséquent, notre appel __callStatic('get', ['/', Closure()]) représente ce que nous avons passé lors de l'appel Route::get() , la route / et un Closure() qui renvoie la vue de bienvenue.


Lorsque __callStatic() est déclenché, il tente d'abord de définir une variable $instance en appelant getFacadeRoot() , la $instance contient la classe réelle à laquelle l'appel doit être transféré, regardons de plus près, cela aura du sens dans un instant

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


Hé, regarde, c'est le getFacadeAccessor() de la classe enfant Route , qui, nous le savons, a renvoyé la chaîne router . Cette chaîne router est ensuite transmise à resolveFacadeInstance() , qui tente de la résoudre en une classe, une sorte de mappage indiquant « Quelle classe cette chaîne représente-t-elle ? » Voyons.

 // 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]; } }

Il vérifie d'abord si un tableau statique, $resolvedInstance , a une valeur définie avec le $name donné (qui, encore une fois, est router ). S'il trouve une correspondance, il renvoie simplement cette valeur. Il s'agit de la mise en cache Laravel pour optimiser un peu les performances. Cette mise en cache se produit au sein d’une seule requête. Si cette méthode est appelée plusieurs fois avec le même argument dans la même requête, elle utilise la valeur mise en cache. Supposons qu'il s'agisse de l'appel initial et procédons.


Il vérifie ensuite si $app est défini et $app est une instance du conteneur d'application

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

Si vous êtes curieux de savoir ce qu'est un conteneur d'application, considérez-le comme une boîte dans laquelle vos cours sont stockés. Lorsque vous avez besoin de ces cours, il vous suffit d'accéder à cette case. Parfois, ce conteneur fait un peu de magie. Même si la boîte est vide et que vous souhaitez prendre un cours, elle l'obtiendra pour vous. C'est un sujet pour un autre article.


Maintenant, vous vous demandez peut-être : "Quand $app est-il défini ?", car cela doit être le cas, sinon nous n'aurons pas notre $instance . Ce conteneur d'application est défini lors du processus de démarrage de notre application. Jetons un coup d'œil rapide à la classe \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()); } } }

Lorsqu'une requête arrive, elle est envoyée au routeur. Juste avant cela, la méthode bootstrap() est invoquée, qui utilise le tableau bootstrappers pour préparer l'application. Si vous explorez la méthode bootstrapWith() dans la classe \Illuminate\Foundation\Application , elle parcourt ces bootstrappers, appelant leur méthode bootstrap() .


Pour plus de simplicité, concentrons-nous simplement sur \Illuminate\Foundation\Bootstrap\RegisterFacades , dont nous savons qu'il contient une méthode bootstrap() qui sera invoquée dans bootstrapWith()

 <?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(); } }


Et voilà, nous définissons le conteneur d'application sur la classe Facade en utilisant la méthode statique setFacadeApplication().

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


Vous voyez, nous attribuons la propriété $app que nous testons dans resolveFacadeInstance() . Cela répond à la question ; nous allons continuer.

 // 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]; } }

Nous avons confirmé que $app est défini lors du démarrage de l'application. L'étape suivante consiste à vérifier si l'instance résolue doit être mise en cache en vérifiant $cached , qui est par défaut true. Enfin, nous récupérons l'instance du conteneur d'application, dans notre cas, c'est comme demander static::$app['router'] de fournir n'importe quelle classe liée au string router .


Maintenant, vous vous demandez peut-être pourquoi nous accédons $app comme un tableau alors qu'il s'agit d'une instance du conteneur d'application, donc d'un object . Eh bien, vous avez raison ! Cependant, le conteneur d'application implémente une interface PHP appelée ArrayAccess , permettant un accès de type tableau. Nous pouvons y jeter un œil pour confirmer ce fait :

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


Ainsi, le resolveFacadeInstance() renvoie en effet une instance liée à la chaîne router , en particulier \Illuminate\Routing\Router . Comment le savais-je ? Jetez un œil à la façade Route ; souvent, vous trouverez un PHPDoc @see faisant allusion à ce que cache cette façade ou, plus précisément, à quelle classe nos appels de méthode seront proxy.


Revenons maintenant à notre méthode __callStatic .

 <?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); } }


Nous avons $instance , un objet de la classe \Illuminate\Routing\Router . Nous testons s'il est défini (ce qui, dans notre cas, est confirmé), et nous invoquons directement la méthode dessus. Donc, on se retrouve avec.

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


Et maintenant, vous pouvez confirmer que get() existe dans la classe \Illuminate\Routing\Router .

 <?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); } }

C'est tout ! N'était-ce pas difficile au final ? Pour récapituler, une façade renvoie une chaîne liée au conteneur. Par exemple, hello-world pourrait être lié à la classe HelloWorld . Lorsque nous invoquons statiquement une méthode non définie sur une façade, HelloWorldFacade par exemple, __callStatic() intervient.


Il résout la chaîne enregistrée dans sa méthode getFacadeAccessor() en tout ce qui est lié au conteneur et transmet notre appel à cette instance récupérée. Ainsi, nous nous retrouvons avec (new HelloWorld())->method() . C'est l'essentiel ! Vous n'avez toujours pas cliqué pour vous ? Créons alors notre façade !

Faisons notre façade

Disons que nous avons cette classe :

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


Le but est d'invoquer HelloWorld::greet() . Pour ce faire, nous allons lier notre classe au conteneur d'application. Tout d’abord, accédez à 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; }); } // ... }


Désormais, chaque fois que nous demandons hello-world à notre conteneur d'application (ou à la boîte, comme je l'ai mentionné plus tôt), il renvoie une instance de HelloWorld . Ce qui reste? Créez simplement une façade qui renvoie la chaîne hello-world .

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


Une fois cela en place, nous sommes prêts à l'utiliser. Appelons-le dans notre web.php.

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


Nous savons que greet() n'existe pas sur la façade HelloWorldFacade , __callStatic() est déclenché. Il extrait une classe représentée par une chaîne ( hello-world dans notre cas) du conteneur d'application. Et nous avons déjà effectué cette liaison dans AppServiceProvider ; nous lui avons demandé de fournir une instance de HelloWorld chaque fois que quelqu'un demande un hello-world . Par conséquent, tout appel, tel que greet() , fonctionnera sur cette instance récupérée de HelloWorld . Et c'est tout.


Toutes nos félicitations! Vous avez créé votre propre façade !

Façades Laravel en temps réel

Maintenant que vous comprenez bien les façades, il vous reste encore un tour de magie à dévoiler. Imaginez pouvoir appeler HelloWorld::greet() sans créer de façade, en utilisant des façades en temps réel .


Regardons:

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


En préfixant l'espace de noms du contrôleur avec Facades , nous obtenons le même résultat que précédemment. Mais il est certain que le contrôleur HelloWorld ne possède pas de méthode statique nommée greet() ! Et d'où vient Facades\App\Http\Controllers\HelloWorld ? Je comprends que cela puisse ressembler à de la sorcellerie, mais une fois que vous l'avez compris, c'est assez simple.


Regardons de plus près \Illuminate\Foundation\Bootstrap\RegisterFacades que nous avons vérifié plus tôt, la classe responsable de la définition de $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 } }


Vous pouvez voir à la toute fin que la méthode register() est invoquée. Jetons un coup d'œil à l'intérieur :

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


La variable $registered est initialement définie sur false . Par conséquent, nous entrons dans l’instruction if et appelons la méthode prependToLoaderStack() . Explorons maintenant sa mise en œuvre.

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


C'est ici que la magie opère ! Laravel appelle la fonction spl_autoload_register() , une fonction PHP intégrée qui se déclenche lors de la tentative d'accès à une classe non définie. Il définit la logique à exécuter dans de telles situations. Dans ce cas, Laravel choisit d'invoquer la méthode load() lorsqu'il rencontre un appel non défini.


De plus, spl_autoload_register() transmet automatiquement le nom de la classe non définie à la méthode ou à la fonction qu'elle appelle.


Explorons la méthode load() ; ça doit être la clé.

 // 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); } }

Nous vérifions si $facadeNamespace est défini et si quelle que soit la classe passée, dans notre cas, Facades\App\Http\Controllers\HelloWorld commence par tout ce qui est défini dans $facadeNamespace


La logique vérifie si $facadeNamespace est défini et si la classe passée, dans notre cas Facades\App\Http\Controllers\HelloWorld (qui n'est pas définie), commence par la valeur spécifiée dans $facadeNamespace.

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


Puisque nous avons préfixé l'espace de noms de notre contrôleur avec Facades , satisfaisant la condition, nous procédons à loadFacade()

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


Ici, la méthode nécessite le chemin renvoyé par ensureFacadeExists() . La prochaine étape consiste donc à approfondir sa mise en œuvre.

 // 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; }


Tout d'abord, une vérification est effectuée pour déterminer si un fichier nommé framework/cache/facade-'.sha1($alias).'.php' existe. Dans notre cas, ce fichier n'est pas présent, déclenchant l'étape suivante : file_put_contents() . Cette fonction crée un fichier et l'enregistre dans le $path spécifié. Le contenu du fichier est généré par formatFacadeStub() , qui, à en juger par son nom, crée une façade à partir d'un stub. Si vous deviez inspecter facade.stub , vous trouveriez ce qui suit :

 <?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'; } }


Cela vous semble familier ? C'est essentiellement ce que nous avons fait manuellement. Désormais, formatFacadeStub() remplace le contenu factice par notre classe non définie après avoir supprimé le préfixe Facades\\ . Cette façade mise à jour est ensuite stockée. Par conséquent, lorsque loadFacade() requiert le fichier, il le fait correctement et finit par nécessiter le fichier suivant :

 <?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'; } }

Et maintenant, dans le flux habituel, nous demandons au conteneur d'application de renvoyer toute instance liée à la chaîne App\Http\Controllers\HelloWorld . Vous vous demandez peut-être si nous n'avons lié cette chaîne à rien, nous n'avons même pas touché à notre AppServiceProvider . Mais rappelez-vous ce que j’ai mentionné au tout début à propos du conteneur d’applications ?


Même si la case est vide, elle renverra l'instance , mais à une condition, la classe ne doit pas avoir de constructeur. Sinon, il ne saurait pas comment le construire pour vous. Dans notre cas, notre classe HelloWorld n’a besoin d’aucun argument pour être construite. Ainsi, le conteneur le résout, le renvoie et tous les appels lui sont transmis par proxy.


Récapitulatif des façades en temps réel : nous avons préfixé notre classe par Facades . Lors du démarrage de l'application, Laravel enregistre spl_autoload_register() , qui se déclenche lorsque nous appelons des classes non définies. Cela conduit finalement à la méthode load() . À l’intérieur load() , nous vérifions si la classe non définie actuelle est préfixée par Facades . Cela correspond, alors Laravel essaie de le charger.


Comme la façade n'existe pas, il la crée à partir d'un stub et requiert ensuite le fichier. Et voilà ! Vous avez une façade classique, mais celle-ci a été créée à la volée. Plutôt cool, hein ?

Conclusion

Félicitations pour être arrivé jusqu'ici ! Je comprends que cela puisse être un peu écrasant. N'hésitez pas à revenir en arrière et à relire les sections qui n'ont pas vraiment cliqué pour vous. Le suivi de votre IDE peut également vous aider. Mais bon, fini la magie noire, ça doit faire du bien, du moins c'est ce que j'ai ressenti la première fois !


Et rappelez-vous, la prochaine fois que vous appellerez une méthode de manière statique, ce ne sera peut-être pas le cas 🪄