paint-brush
Laravel Under The Hood - O que são fachadas?por@oussamamater
Novo histórico

Laravel Under The Hood - O que são fachadas?

por Oussama Mater14m2024/06/29
Read on Terminal Reader

Muito longo; Para ler

O Laravel vem com muitas fachadas que usamos com frequência. Discutiremos o que são, como podemos criar nossas próprias Fachadas e também aprenderemos sobre Fachadas em tempo real.
featured image - Laravel Under The Hood - O que são fachadas?
Oussama Mater HackerNoon profile picture

Você acabou de instalar um novo aplicativo Laravel, inicializá-lo e obter a página de boas-vindas. Como todo mundo, você tenta ver como ele é renderizado, então entra no arquivo web.php e encontra este código:

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

É óbvio como obtivemos a visão de boas-vindas, mas você está curioso para saber como funciona o roteador do Laravel, então decide mergulhar no código. A suposição inicial é: há uma classe Route na qual estamos chamando um método estático get() . No entanto, ao clicar nele, não existe o método get() ali. Então, que tipo de magia negra está acontecendo? Vamos desmistificar isso!

Fachadas Regulares

Observe que removi a maior parte dos PHPDocs e incorporei os tipos apenas para simplificar, "..." refere-se a mais código.

Eu sugiro fortemente abrir seu IDE e acompanhar o código para evitar qualquer confusão.


Seguindo nosso exemplo, vamos explorar a classe Route .

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


Não há muito aqui, apenas o método getFacadeAccessor() que retorna a string router . Tendo isso em mente, vamos passar para a classe pai.

 <?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 da classe pai, existem muitos métodos, mas não existe um método get() . Mas há um interessante, o método __callStatic() . É um método mágico , invocado sempre que um método estático indefinido, como get() no nosso caso, é chamado. Portanto, nossa chamada __callStatic('get', ['/', Closure()]) representa o que passamos ao invocar Route::get() , a rota / e um Closure() que retorna a visualização de boas-vindas.


Quando __callStatic() é acionado, ele primeiro tenta definir uma variável $instance chamando getFacadeRoot() , o $instance contém a classe real para a qual a chamada deve ser encaminhada, vamos dar uma olhada mais de perto, isso fará sentido daqui a pouco

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


Ei, veja, é o getFacadeAccessor() da classe filha Route , que sabemos que retornou a string router . Essa string router é então passada para resolveFacadeInstance() , que tenta resolvê-la para uma classe, uma espécie de mapeamento que diz "Que classe esta string representa?" Vamos 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]; } }

Ele primeiro verifica se um array estático, $resolvedInstance , tem um valor definido com o $name fornecido (que, novamente, é router ). Se encontrar uma correspondência, apenas retornará esse valor. Este é o cache do Laravel para otimizar um pouco o desempenho. Esse armazenamento em cache ocorre em uma única solicitação. Se esse método for chamado várias vezes com o mesmo argumento na mesma solicitação, ele usará o valor armazenado em cache. Vamos supor que seja a chamada inicial e prosseguir.


Em seguida, ele verifica se $app está definido e $app é uma instância do contêiner do aplicativo

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

Se você está curioso para saber o que é um contêiner de aplicação, pense nele como uma caixa onde suas classes são armazenadas. Quando você precisar dessas aulas, basta acessar essa caixa. Às vezes, esse contêiner faz um pouco de mágica. Mesmo que a caixa esteja vazia e você procure uma aula, ela a receberá para você. Isso é assunto para outro artigo.


Agora, você pode se perguntar: "Quando $app é definido?", porque precisa ser, caso contrário, não teremos nosso $instance . Este contêiner de aplicativo é definido durante o processo de inicialização do nosso aplicativo. Vamos dar uma olhada rápida na 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()); } } }

Quando uma solicitação chega, ela é enviada ao roteador. Pouco antes disso, o método bootstrap() é invocado, que usa o array bootstrappers para preparar a aplicação. Se você explorar o método bootstrapWith() na classe \Illuminate\Foundation\Application , ele itera por meio desses bootstrappers, chamando seu método bootstrap() .


Para simplificar, vamos nos concentrar apenas em \Illuminate\Foundation\Bootstrap\RegisterFacades , que sabemos que contém um método bootstrap() que será invocado em 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(); } }


E aí está, estamos configurando o contêiner do aplicativo na classe Facade usando o método estático setFacadeApplication().

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


Veja, atribuímos a propriedade $app que estamos testando em resolveFacadeInstance() . Isso responde à pergunta; vamos continuar.

 // 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 está definido durante a inicialização do aplicativo. A próxima etapa é verificar se a instância resolvida deve ser armazenada em cache, verificando $cached , cujo padrão é verdadeiro. Por fim, recuperamos a instância do contêiner do aplicativo; em nosso caso, é como pedir static::$app['router'] para fornecer qualquer classe vinculada à string router .


Agora, você deve estar se perguntando por que acessamos $app como um array, apesar de ser uma instância do contêiner do aplicativo, portanto, um object . Bem, você está certo! No entanto, o contêiner do aplicativo implementa uma interface PHP chamada ArrayAccess , permitindo acesso semelhante a um array. Podemos dar uma olhada para confirmar este fato:

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


Portanto, resolveFacadeInstance() de fato retorna uma instância vinculada à string router , especificamente \Illuminate\Routing\Router . Como eu sabia? Dê uma olhada na fachada Route ; frequentemente, você encontrará um PHPDoc @see sugerindo o que essa fachada esconde ou, mais precisamente, para qual classe nossas chamadas de método serão proxy.


Agora, de volta ao nosso 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); } }


Temos $instance , um objeto da classe \Illuminate\Routing\Router . Testamos se está definido (o que, no nosso caso, está confirmado) e invocamos diretamente o método nele. Então, terminamos com.

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


E agora, você pode confirmar que get() existe na 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); } }

Isso encerra tudo! Não foi tão difícil no final? Para recapitular, uma fachada retorna uma string vinculada ao contêiner. Por exemplo, hello-world pode estar vinculado à classe HelloWorld . Quando invocamos estaticamente um método indefinido em uma fachada, HelloWorldFacade , por exemplo, __callStatic() intervém.


Ele resolve a string registrada em seu método getFacadeAccessor() para o que quer que esteja vinculado ao contêiner e faz proxy de nossa chamada para essa instância recuperada. Assim, terminamos com (new HelloWorld())->method() . Essa é a essência disso! Ainda não clicou para você? Vamos criar nossa fachada então!

Vamos fazer nossa fachada

Digamos que temos esta classe:

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


O objetivo é invocar HelloWorld::greet() . Para fazer isso, vincularemos nossa classe ao contêiner do aplicativo. Primeiro, navegue até 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; }); } // ... }


Agora, sempre que solicitamos hello-world do contêiner do nosso aplicativo (ou da caixa, como mencionei anteriormente), ele retorna uma instância de HelloWorld . O que sobrou? Basta criar uma fachada que retorne a string hello-world .

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


Com isso implementado, estamos prontos para usá-lo. Vamos chamá-lo dentro do nosso web.php.

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


Sabemos que greet() não existe na fachada HelloWorldFacade , __callStatic() é acionado. Ele extrai uma classe representada por uma string ( hello-world no nosso caso) do contêiner do aplicativo. E já fizemos essa vinculação no AppServiceProvider ; nós o instruímos a fornecer uma instância de HelloWorld sempre que alguém solicitar um hello-world . Conseqüentemente, qualquer chamada, como greet() , operará na instância recuperada de HelloWorld . E é isso.


Parabéns! Você criou sua própria fachada!

Fachadas em tempo real do Laravel

Agora que você conhece bem as fachadas, há mais um truque de mágica para desvendar. Imagine poder chamar HelloWorld::greet() sem criar uma fachada, usando fachadas em tempo real .


Vamos dar uma olhada:

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


Ao prefixar o namespace do controlador com Facades , obtemos o mesmo resultado anterior. Mas é certo que o controlador HelloWorld não possui nenhum método estático chamado greet() ! E de onde vem Facades\App\Http\Controllers\HelloWorld ? Eu entendo que isso pode parecer algum tipo de feitiçaria, mas uma vez que você entende, é bem simples.


Vamos dar uma olhada mais de perto em \Illuminate\Foundation\Bootstrap\RegisterFacades que verificamos anteriormente, a classe responsável por configurar o $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 } }


Você pode ver no final que o método register() é invocado. Vamos dar uma olhada por dentro:

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


A variável $registered é inicialmente definida como false . Portanto, inserimos a instrução if e chamamos o método prependToLoaderStack() . Agora, vamos explorar sua implementação.

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


É aqui que a mágica acontece! Laravel está chamando a função spl_autoload_register() , uma função PHP integrada que é acionada ao tentar acessar uma classe indefinida. Ele define a lógica a ser executada em tais situações. Neste caso, o Laravel escolhe invocar o método load() ao encontrar uma chamada indefinida.


Além disso, spl_autoload_register() passa automaticamente o nome da classe indefinida para qualquer método ou função que ela chamar.


Vamos explorar o método load() ; deve ser a chave.

 // 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 se $facadeNamespace está definido e se qualquer classe passada, no nosso caso, Facades\App\Http\Controllers\HelloWorld começa com o que está definido em $facadeNamespace


A lógica verifica se $facadeNamespace está definido e se a classe passada, em nosso caso Facades\App\Http\Controllers\HelloWorld (que é indefinida), começa com o valor especificado em $facadeNamespace.

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


Como prefixamos o namespace do nosso controlador com Facades , satisfazendo a condição, prosseguimos para loadFacade()

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


Aqui, o método requer qualquer caminho retornado de ensureFacadeExists() . Portanto, o próximo passo é aprofundar sua implementação.

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


Primeiro, é feita uma verificação para verificar se um arquivo chamado framework/cache/facade-'.sha1($alias).'.php' existe. No nosso caso, este arquivo não está presente, acionando a próxima etapa: file_put_contents() . Esta função cria um arquivo e o salva no $path especificado. O conteúdo do arquivo é gerado por formatFacadeStub() , que, a julgar pelo nome, cria uma fachada a partir de um stub. Se você inspecionasse facade.stub , encontraria o seguinte:

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


Parece familiar? Isso é essencialmente o que fizemos manualmente. Agora, formatFacadeStub() substitui o conteúdo fictício por nossa classe indefinida após remover o prefixo Facades\\ . Esta fachada atualizada é então armazenada. Conseqüentemente, quando loadFacade() requer o arquivo, ele o faz corretamente e acaba exigindo o seguinte arquivo:

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

E agora, no fluxo normal, pedimos ao contêiner do aplicativo que retorne qualquer instância vinculada à string App\Http\Controllers\HelloWorld . Você deve estar se perguntando: não vinculamos essa string a nada, nem tocamos em nosso AppServiceProvider . Mas lembra do que mencionei sobre o contêiner do aplicativo logo no início?


Mesmo que a caixa esteja vazia, ela retornará a instância , mas com uma condição, a classe não deve ter construtor. Caso contrário, não saberia como construí-lo para você. No nosso caso, nossa classe HelloWorld não precisa de nenhum argumento para ser construída. Assim, o contêiner resolve, retorna e todas as chamadas são enviadas por proxy para ele.


Recapitulando fachadas em tempo real: prefixamos nossa classe com Facades . Durante a inicialização da aplicação, o Laravel registra spl_autoload_register() , que é acionado quando chamamos classes indefinidas. Eventualmente leva ao método load() . Dentro de load() , verificamos se a classe indefinida atual tem o prefixo Facades . Ele corresponde, então o Laravel tenta carregá-lo.


Como a fachada não existe, ela é criada a partir de um stub e então requer o arquivo. E pronto! Você tem uma fachada normal, mas esta foi criada na hora. Muito legal, hein?

Conclusão

Parabéns por chegar até aqui! Eu entendo que pode ser um pouco opressor. Sinta-se à vontade para voltar e reler quaisquer seções que não tenham agradado a você. Acompanhar seu IDE também pode ajudar. Mas ei, chega de magia negra, deve ser bom, pelo menos foi assim que me senti da primeira vez!


E lembre-se, da próxima vez que você chamar um método estaticamente, pode não ser o caso 🪄