Acaba de instalar una aplicación Laravel nueva, la inició y obtuvo la página de bienvenida. Como todos los demás, intentas ver cómo se representa, así que ingresas al archivo web.php
y encuentras este código:
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); });
Es obvio cómo obtuvimos la vista de bienvenida, pero tienes curiosidad sobre cómo funciona el enrutador de Laravel, así que decides sumergirte en el código. La suposición inicial es: hay una clase Route
en la que llamamos a un método estático get()
. Sin embargo, al hacer clic en él, no hay ningún método get()
allí. Entonces, ¿qué tipo de magia oscura está ocurriendo? ¡Desmitifiquemos esto!
Tenga en cuenta que eliminé la mayoría de los PHPDocs y alineé los tipos solo por simplicidad, "..." se refiere a más código.
Le recomiendo encarecidamente que abra su IDE y siga el código para evitar confusiones.
Siguiendo nuestro ejemplo, exploremos la clase Route
.
<?php namespace Illuminate\Support\Facades; class Route extends Facade { // ... protected static function getFacadeAccessor(): string { return 'router'; } }
No hay mucho aquí, solo el método getFacadeAccessor()
que devuelve la cadena router
. Teniendo esto en cuenta, pasemos a la clase para padres.
<?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); } }
Dentro de la clase principal, hay muchos métodos, aunque no existe un método get()
. Pero hay uno interesante, el método __callStatic()
. Es un método mágico , que se invoca cada vez que se llama a un método estático indefinido, como get()
en nuestro caso. Por lo tanto, nuestra llamada __callStatic('get', ['/', Closure()])
representa lo que pasamos al invocar Route::get()
, la ruta /
y un Closure()
que devuelve la vista de bienvenida.
Cuando se activa __callStatic()
, primero intenta establecer una variable $instance
llamando getFacadeRoot()
, $instance
contiene la clase real a la que se debe reenviar la llamada, echemos un vistazo más de cerca, tendrá sentido en un momento
// Facade.php public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); }
Oye, mira, es getFacadeAccessor()
de la clase secundaria Route
, que sabemos que devolvió la cadena router
. Esta cadena router
luego se pasa a resolveFacadeInstance()
, que intenta resolverla en una clase, una especie de mapeo que dice "¿Qué clase representa esta cadena?" Vamos a ver.
// 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]; } }
Primero verifica si una matriz estática, $resolvedInstance
, tiene un valor establecido con el $name
dado (que, nuevamente, es router
). Si encuentra una coincidencia, simplemente devuelve ese valor. Este es el almacenamiento en caché de Laravel para optimizar un poco el rendimiento. Este almacenamiento en caché se produce dentro de una única solicitud. Si se llama a este método varias veces con el mismo argumento dentro de la misma solicitud, utiliza el valor almacenado en caché. Supongamos que es la llamada inicial y procedamos.
Luego verifica si $app
está configurado y $app
es una instancia del contenedor de la aplicación.
// Facade.php protected static \Illuminate\Contracts\Foundation\Application $app;
Si tiene curiosidad acerca de qué es un contenedor de aplicaciones, considérelo como una caja donde se almacenan sus clases. Cuando necesites esas clases, simplemente buscas en esa casilla. A veces, este contenedor realiza un poco de magia. Incluso si la caja está vacía y buscas una clase, te la conseguirá. Ese es un tema para otro artículo.
Ahora, quizás te preguntes: "¿Cuándo se establece $app
?", porque es necesario que así sea; de lo contrario, no tendremos nuestra $instance
. Este contenedor de aplicaciones se configura durante el proceso de arranque de nuestra aplicación. Echemos un vistazo rápido a la clase \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()); } } }
Cuando llega una solicitud, se envía al enrutador. Justo antes de eso, se invoca el método bootstrap()
, que utiliza la matriz bootstrappers
para preparar la aplicación. Si explora el método bootstrapWith()
en la clase \Illuminate\Foundation\Application
, itera a través de estos bootstrappers, llamando a su método bootstrap()
.
Para simplificar, centrémonos en \Illuminate\Foundation\Bootstrap\RegisterFacades
, que sabemos que contiene un método bootstrap()
que se invocará en 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(); } }
Y ahí está, estamos configurando el contenedor de la aplicación en la clase Facade
usando el método estático setFacadeApplication().
// RegisterFacades.php public static function setFacadeApplication($app) { static::$app = $app; }
Mira, asignamos la propiedad $app
que estamos probando dentro de resolveFacadeInstance()
. Esto responde a la pregunta; continuemos.
// 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]; } }
Confirmamos que $app
se configura durante el arranque de la aplicación. El siguiente paso es verificar si la instancia resuelta debe almacenarse en caché verificando $cached
, cuyo valor predeterminado es verdadero. Finalmente, recuperamos la instancia del contenedor de la aplicación; en nuestro caso, es como pedirle static::$app['router']
que proporcione cualquier clase vinculada a la cadena router
.
Ahora bien, quizás te preguntes por qué accedemos $app
como una matriz a pesar de ser una instancia del contenedor de la aplicación, es decir, un objeto . Bueno, ¡tienes razón! Sin embargo, el contenedor de la aplicación implementa una interfaz PHP llamada ArrayAccess
, que permite un acceso similar a una matriz. Podemos echarle un vistazo para confirmar este hecho:
<?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... }
Entonces, resolveFacadeInstance()
de hecho devuelve una instancia vinculada a la cadena router
, específicamente, \Illuminate\Routing\Router
. ¿Cómo lo supe? Echa un vistazo a la fachada Route
; a menudo, encontrará un PHPDoc @see
que insinúa lo que oculta esta fachada o, más precisamente, a qué clase se enviarán las llamadas a nuestro método.
Ahora, volvamos a nuestro método __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); } }
Tenemos $instance
, un objeto de la clase \Illuminate\Routing\Router
. Probamos si está configurado (lo cual, en nuestro caso, se confirma) e invocamos directamente el método en él. Entonces terminamos con.
// Facade.php return $instance->get('/', Closure());
Y ahora, puede confirmar que get()
existe dentro de la clase \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); } }
¡Eso concluye! ¿No fue tan difícil al final? En resumen, una fachada devuelve una cadena vinculada al contenedor. Por ejemplo, hello-world
podría estar vinculado a la clase HelloWorld
. Cuando invocamos estáticamente un método no definido en una fachada, HelloWorldFacade
, por ejemplo, interviene __callStatic()
.
Resuelve la cadena registrada en su método getFacadeAccessor()
a lo que esté vinculado dentro del contenedor y envía nuestra llamada a esa instancia recuperada. Por lo tanto, terminamos con (new HelloWorld())->method()
. ¡Esa es la esencia! ¿Aún no has hecho clic para ti? ¡Vamos a crear nuestra fachada entonces!
Digamos que tenemos esta clase:
<?php namespace App\Http\Controllers; class HelloWorld { public function greet(): string { return "Hello, World!"; } }
El objetivo es invocar HelloWorld::greet()
. Para hacer esto, vincularemos nuestra clase al contenedor de la aplicación. Primero, navegue hasta 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; }); } // ... }
Ahora, cada vez que solicitamos hello-world
desde el contenedor de nuestra aplicación (o el cuadro, como mencioné anteriormente), devuelve una instancia de HelloWorld
. ¿Lo que queda? Simplemente crea una fachada que devuelva la cadena hello-world
.
<?php namespace App\Http\Facades; use Illuminate\Support\Facades\Facade; class HelloWorldFacade extends Facade { protected static function getFacadeAccessor() { return 'hello-world'; } }
Con esto en su lugar, estamos listos para usarlo. Llamémoslo dentro de nuestro web.php.
<?php use App\Http\Facades; use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorldFacade::greet(); // Hello, World! });
Sabemos que greet()
no existe en la fachada HelloWorldFacade
, se activa __callStatic()
. Extrae una clase representada por una cadena ( hello-world
en nuestro caso) del contenedor de la aplicación. Y ya hemos hecho este enlace en AppServiceProvider
; Le indicamos que proporcionara una instancia de HelloWorld
cada vez que alguien solicite un hello-world
. En consecuencia, cualquier llamada, como greet()
, funcionará en esa instancia recuperada de HelloWorld
. Y eso es.
¡Felicidades! ¡Has creado tu propia fachada!
Ahora que comprendes bien las fachadas, te queda un truco de magia más por descubrir. Imagine poder llamar HelloWorld::greet()
sin crear una fachada, usando fachadas en tiempo real .
Echemos un vistazo:
<?php use Facades\App\Http\Controllers; // Notice the prefix use Illuminate\Support\Facades\Route; Route::get('/', function () { return HelloWorld::greet(); // Hello, World! });
Al anteponer Facades
al espacio de nombres del controlador, logramos el mismo resultado que antes. ¡Pero es seguro que el controlador HelloWorld
no tiene ningún método estático llamado greet()
! ¿Y de dónde viene Facades\App\Http\Controllers\HelloWorld
? Entiendo que esto pueda parecer una especie de brujería, pero una vez que lo entiendes, es bastante simple.
Echemos un vistazo más de cerca a \Illuminate\Foundation\Bootstrap\RegisterFacades
que verificamos anteriormente, la clase responsable de configurar $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 } }
Puede ver al final que se invoca el método register()
. Echemos un vistazo al interior:
<?php namespace Illuminate\Foundation; class AliasLoader { // ... protected $registered = false; public function register(): void { if (! $this->registered) { $this->prependToLoaderStack(); $this->registered = true; } } }
La variable $registered
se establece inicialmente en false
. Por lo tanto, ingresamos la declaración if
y llamamos al método prependToLoaderStack()
. Ahora, exploremos su implementación.
// AliasLoader.php protected function prependToLoaderStack(): void { spl_autoload_register([$this, 'load'], true, true); }
¡Aquí es donde ocurre la magia! Laravel está llamando a la función spl_autoload_register()
, una función PHP incorporada que se activa cuando se intenta acceder a una clase no definida. Define la lógica a ejecutar en tales situaciones. En este caso, Laravel elige invocar el método load()
cuando encuentra una llamada indefinida.
Además, spl_autoload_register()
pasa automáticamente el nombre de la clase no definida a cualquier método o función que llame.
Exploremos el método load()
; debe ser la clave.
// 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); } }
Verificamos si $facadeNamespace
está configurado, y si cualquier clase pasó, en nuestro caso, Facades\App\Http\Controllers\HelloWorld
comienza con lo que esté configurado en $facadeNamespace
La lógica verifica si $facadeNamespace
está configurado y si la clase pasada, en nuestro caso Facades\App\Http\Controllers\HelloWorld
(que no está definida), comienza con el valor especificado en $facadeNamespace.
// AliasLoader.php protected static $facadeNamespace = 'Facades\\';
Como le hemos puesto el prefijo Facades
al espacio de nombres de nuestro controlador, cumpliendo la condición, procedemos a loadFacade()
// AliasLoader.php protected function loadFacade($alias) { require $this->ensureFacadeExists($alias); }
Aquí, el método requiere cualquier ruta devuelta desde ensureFacadeExists()
. Entonces, el siguiente paso es profundizar en su implementación.
// 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; }
Primero, se realiza una verificación para determinar si existe un archivo llamado framework/cache/facade-'.sha1($alias).'.php'
. En nuestro caso, este archivo no está presente, lo que desencadena el siguiente paso: file_put_contents()
. Esta función crea un archivo y lo guarda en la $path
especificada. El contenido del archivo es generado por formatFacadeStub()
, que, a juzgar por su nombre, crea una fachada a partir de un código auxiliar. Si inspeccionaras facade.stub
, encontrarías lo siguiente:
<?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'; } }
¿Luce familiar? Eso es esencialmente lo que hicimos manualmente. Ahora, formatFacadeStub()
reemplaza el contenido ficticio con nuestra clase no definida después de eliminar el prefijo Facades\\
. Esta fachada actualizada luego se almacena. En consecuencia, cuando loadFacade()
requiere el archivo, lo hace correctamente y termina requiriendo el siguiente archivo:
<?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'; } }
Y ahora, en el flujo habitual, le pedimos al contenedor de la aplicación que devuelva cualquier instancia vinculada a la cadena App\Http\Controllers\HelloWorld
. Quizás se pregunte: no vinculamos esta cadena a nada, ni siquiera tocamos nuestro AppServiceProvider
. ¿Pero recuerdas lo que mencioné sobre el contenedor de la aplicación al principio?
Incluso si el cuadro está vacío, devolverá la instancia , pero con una condición, la clase no debe tener un constructor. De lo contrario, no sabría cómo construirlo para usted. En nuestro caso, nuestra clase HelloWorld
no necesita ningún argumento para construirse. Entonces, el contenedor lo resuelve, lo devuelve y todas las llamadas pasan a él.
Recapitulando fachadas en tiempo real: hemos antepuesto a nuestra clase Facades
. Durante el arranque de la aplicación, Laravel registra spl_autoload_register()
, que se activa cuando llamamos a clases no definidas. Eventualmente conduce al método load()
. Dentro de load()
, verificamos si la clase indefinida actual tiene el prefijo Facades
. Coincide, por lo que Laravel intenta cargarlo.
Como la fachada no existe, la crea a partir de un código auxiliar y luego requiere el archivo. ¡Y voilá! Tienes una fachada normal, pero ésta se creó sobre la marcha. Muy bien, ¿eh?
¡Felicidades por haber llegado tan lejos! Entiendo que puede ser un poco abrumador. Siéntase libre de regresar y volver a leer cualquier sección que no le haya parecido del todo satisfactoria. Hacer un seguimiento de su IDE también puede ayudar. Pero bueno, no más magia negra, debe sentirse bien, ¡al menos así me sentí la primera vez!
Y recuerda, la próxima vez que llames a un método estáticamente, puede que no sea el caso 🪄