Laravel под капотом: Facades
- Привет,
Facades
👋 - Обычные фасады
- Давайте создадим свой Фасад
- Laravel Facades реального времени
- Заключение
Привет, Facades
👋
Вы только что установили свежее Laravel приложение, загрузили его и получили страницу приветствия. Как и все остальные, вы пытаетесь посмотреть, как она отображается, поэтому заходите в файл web.php и встречаете следующий код
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Очевидно, как мы получили представление/view приветствия, но вам интересно, как работает маршрутизатор Laravel, и вы решили погрузиться в код. Первоначальное предположение таково: существует класс Route
, у которого мы вызываем статический метод get()
. Однако при нажатии на него метод get()
отсутствует. Так что же за тёмная магия происходит? Давайте разберёмся!
Обычные фасады
Обратите внимание, что я удалил большую часть PHPDocs и вставил для упрощения "...", это относится к большему количеству кода.
Я настоятельно рекомендую открыть вашу IDE и изучить код, во избежание путаницы.
Следуя нашему примеру, давайте изучим класс Route
<?php
namespace Illuminate\Support\Facades;
class Route extends Facade
{
// ...
protected static function getFacadeAccessor(): string
{
return 'router';
}
}
Здесь нет ничего особенного, только метод getFacadeAccessor()
, возвращающий строку router
. Запомнив это, перейдём к родительскому классу
<?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);
}
}
Внутри родительского класса есть множество методов, но нет метода get()
. Но есть один интересный метод — __callStatic()
. Это магический метод, вызываемый всякий раз, когда вызывается неопределённый статический метод, как в нашем случае get()
. Поэтому наш вызов __callStatic('get', ['/', Closure()])
представляет собой то, что мы передали при вызове Route::get()
, маршрут /
и Closure()
, который возвращает приветственное представление.
Когда срабатывает __callStatic()
, сначала он пытается установить переменную $instance
, вызывая getFacadeRoot()
. В $instance
хранится реальный класс, к которому должен быть направлен вызов, давайте посмотрим на это подробнее, скоро всё станет ясно.
// Facade.php
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
Смотрите, это getFacadeAccessor()
из дочернего класса Route
, который, как мы знаем, вернул строку router
. Эта строка router
затем передаётся в resolveFacadeInstance()
, пытающийся преобразовать её в класс, своего рода маппинг, который говорит: Какой класс представляет эта строка?
, давайте посмотрим.
// 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];
}
}
Сначала он проверяет, есть ли в статическом массиве $resolvedInstance
значение, заданное $name
(которое, опять, является router
). Если он находит совпадение, то просто возвращает это значение. Это кэширование Laravel, чтобы немного оптимизировать производительность. Кэширование происходит в рамках одного запроса, если этот метод вызывается несколько раз с одним и тем же аргументом в рамках одного запроса, он использует кэшированное значение. Давайте предположим, что это первый вызов, и продолжим.
Затем проверяется, установлено ли значение $app
, и является ли $app
экземпляром контейнера приложения.
// Facade.php
protected static \Illuminate\Contracts\Foundation\Application $app;
Если вам интересно, что такое контейнер приложения, думайте о нем как о коробке, в которой хранятся ваши классы. Когда вам нужны эти классы, вы просто тянетесь к этой коробке. Иногда этот контейнер проявляет некоторую магию, даже если ящик пуст, и вы тянетесь к нему, чтобы взять класс, он достанет его для вас. Это тема для другой статьи.
Теперь вы можете задаться вопросом: Когда задаётся
, потому что он должен быть задан, иначе у нас не будет нашего $app
?$instance
. Контейнер приложения задаётся во время процесса загрузки нашего приложения. Давайте посмотрим на класс \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, // <- этот парень
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
public function bootstrap(): void
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
}
Когда поступает запрос, он отправляется в маршрутизатор. Непосредственно перед этим вызывается метод bootstrap()
, использующий массив bootstrappers для подготовки приложения. Если вы изучите метод bootstrapWith()
в классе \Illuminate\Foundation\Application
, то увидите, что он перебирает эти бутстрапперы, вызывая их метод bootstrap()
. Для простоты остановимся на \Illuminate\Foundation\Bootstrap\RegisterFacades
, как мы знаем, содержащий метод bootstrap()
, который будет вызван в 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); // Интересное здесь
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
}
И вот он, мы задаём контейнер приложения в классе Facade
с помощью статического метода setFacadeApplication()
.
// RegisterFacades.php
public static function setFacadeApplication($app)
{
static::$app = $app;
}
Видите, мы задаём свойство $app
, которое тестируем, в resolveFacadeInstance()
. Это ответ на вопрос, давайте продолжим
// 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];
}
}
Мы подтвердили, что $app
задан во время загрузки приложения. Следующим шагом будет проверка того, нужно ли кэшировать разрешённый экземпляр, для чего проверяем $cached
, по умолчанию имеющий значение true
. Наконец, мы получаем экземпляр из контейнера приложения, в нашем случае это как запрос к static::$app['router']
на предоставление любого класса, связанного со строкой router
. Теперь вы можете удивиться, почему мы обращаемся к $app
как к массиву, несмотря на то, что это экземпляр контейнера приложения, то есть объект. Что ж, вы правы! Однако контейнер приложений реализует PHP-интерфейс ArrayAccess
, позволяющий обращаться к нему как к массиву. Мы можем взглянуть на него, чтобы подтвердить этот факт
<?php
namespace Illuminate\Container;
use ArrayAccess; // <- этот парень
use Illuminate\Contracts\Container\Container as ContainerContract;
class Container implements ArrayAccess, ContainerContract {
// ...
}
Итак, resolveFacadeInstance()
действительно возвращает экземпляр, привязанный к строке router
, а именно, \Illuminate\Routing\Router
. Как я узнал? Посмотрите на фасад Route
, и вы обнаружите в PHPDoc @see
, намекающий на то, что скрывает этот фасад, или, точнее, на то, к какому классу будут проксироваться вызовы наших методов.
Теперь вернёмся к нашему методу __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);
}
}
У нас есть $instance
, объект класса \Illuminate\Routing\Router
. Мы проверяем, установлен ли он (что в нашем случае подтверждается), и напрямую вызываем метод на нем. В итоге мы получаем
// Facade.php
return $instance->get('/', Closure());
И теперь вы можете подтвердить, что get()
существует в классе \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);
}
}
Вот и всё! Не так уж и сложно, в конце концов? Напомним, что фасад возвращает строку, привязанную к контейнеру. Например, hello-world
может быть связана с классом HelloWorld
. Когда мы статически вызываем неопределённый метод фасада, например HelloWorldFacade
, в дело вступает __callStatic()
. Она разрешает строку, зарегистрированную в методе getFacadeAccessor()
, в то, что связано внутри контейнера, и проксирует наш вызов этому полученному экземпляру. Таким образом, в итоге мы получаем (new HelloWorld())->method()
. Вот и вся суть! Вы всё ещё не поняли? Тогда давайте создадим наш фасад!
Давайте создадим свой Фасад
Допустим, у нас есть класс
<?php
namespace App\Http\Controllers;
class HelloWorld
{
public function greet(): string {
return "Hello, World!";
}
}
Задача состоит в том, чтобы вызвать HelloWorld::greet()
. Для этого мы привяжем наш класс к контейнеру приложения. Сначала перейдите к 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;
});
}
// ...
}
Теперь, когда мы запрашиваем hello-world
у нашего контейнера приложений (или коробки, как я уже говорил), он возвращает экземпляр HelloWorld
. Что остаётся? Просто создать фасад, возвращающий строку hello-world
.
<?php
namespace App\Http\Facades;
use Illuminate\Support\Facades\Facade;
class HelloWorldFacade extends Facade
{
protected static function getFacadeAccessor()
{
return 'hello-world';
}
}
Всё готово к использованию. Давайте вызовем его в нашем файле web.php
<?php
use App\Http\Facades;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return HelloWorldFacade::greet(); // Hello, World!
});
Мы знаем, что greet()
не существует в фасаде HelloWorldFacade
, поэтому срабатывает __callStatic()
. Она извлекает класс, представленный строкой (в нашем случае hello-world
), из контейнера приложения. И мы уже сделали эту привязку в AppServiceProvider
, поручив ему предоставлять экземпляр HelloWorld
всякий раз, когда кто-то запрашивает hello-world
. Следовательно, любой вызов, например greet()
, будет работать с этим полученным экземпляром HelloWorld
. Вот и всё.
Поздравляем! Вы создали свой собственный фасад!
Laravel Facades реального времени
Теперь, когда вы хорошо понимаете, что такое фасады, осталось раскрыть ещё один магический трюк. Представьте, что вы можете вызвать HelloWorld::greet()
без создания фасада, используя фасады реального времени.
Давайте посмотрим
<?php
use Facades\App\Http\Controllers; // Обратите внимание на префикс
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return HelloWorld::greet(); // Hello, World!
});
Добавив к пространству имён контроллеров префикс Facades
, мы получим тот же результат, что и раньше. Но, конечно, контроллер HelloWorld
не имеет статического метода greet()
! И откуда вообще взялись Facades\App\Http\Controllers\HelloWorld
? Я понимаю, что это может показаться каким-то колдовством, но как только вы поймёте, всё окажется довольно просто.
Давайте подробнее рассмотрим класс \Illuminate\Foundation\Bootstrap\RegisterFacades
, который мы рассматривали ранее, класс, отвечающий за установку $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(); // Интересное здесь
}
}
Вы видите, что в самом конце вызывается метод register()
. Давайте заглянем внутрь
<?php
namespace Illuminate\Foundation;
class AliasLoader
{
// ...
protected $registered = false;
public function register(): void
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
}
Изначально переменная $registered
имеет значение false
. Поэтому мы вводим оператор if
и вызываем метод prependToLoaderStack()
. Теперь давайте рассмотрим его реализацию
// AliasLoader.php
protected function prependToLoaderStack(): void
{
spl_autoload_register([$this, 'load'], true, true);
}
Вот где происходит магия! Laravel вызывает функцию spl_autoload_register()
, встроенную PHP функцию, срабатывающую при попытке доступа к неопределённому классу. Она определяет логику действий в таких ситуациях. В данном случае Laravel предпочитает вызывать метод load()
при встрече с неопределённым вызовом. Кроме того, функция spl_autoload_register()
автоматически передаёт имя неопределённого класса тому методу или функции, которые она вызывает.
Давайте изучим метод load()
, он должен быть ключевым.
// 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);
}
}
Логика проверяет, установлен ли $facadeNamespace
и начинается ли переданный класс, в нашем случае Facades\App\Http\Controllers\HelloWorld
(который не определён), со значения, указанного в $facadeNamespace
// AliasLoader.php
protected static $facadeNamespace = 'Facades\\';
Поскольку мы присвоили пространству имён нашего контроллера префикс Facades
, удовлетворяющий условию, мы переходим к loadFacade()
.
// AliasLoader.php
protected function loadFacade($alias)
{
require $this->ensureFacadeExists($alias);
}
Здесь метод требует путь, возвращаемый функцией ensureFacadeExists()
. Поэтому следующим шагом будет изучение её реализации
// 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;
}
Сначала проверяется, существует ли файл с именем framework/cache/facade-'.sha1($alias).'.php'
. В нашем случае этот файл отсутствует, что вызывает следующий шаг: file_put_contents()
. Эта функция создаёт файл и сохраняет его в указанный $path
. Содержимое файла генерируется функцией formatFacadeStub()
, которая, судя по названию, создаёт фасад из заглушки. Если просмотреть файл facade.stub
, то можно обнаружить следующее
<?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';
}
}
Выглядит знакомо? По сути, это то, что мы делали вручную. Теперь функция formatFacadeStub()
заменяет фиктивный контент на наш неопределённый класс после удаления префикса Facades\\
. Затем этот обновлённый фасад сохраняется. Следовательно, когда loadFacade()
требует файл, она делает это правильно, и в итоге требуется следующий файл
<?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';
}
}
А теперь, как обычно, мы просим контейнер приложения вернуть любой экземпляр, привязанный к строке App\Http\Controllers\HelloWorld
. Вы можете удивиться, ведь мы ни к чему не привязывали эту строку, мы даже не трогали наш AppServiceProvider
. Но помните, что я говорил о контейнере приложений в самом начале? Даже если ящик пуст, он вернёт экземпляр, но с одним условием — у класса не должно быть конструктора. Иначе он не будет знать, как создать его для вас. В нашем случае класс HelloWorld
не нуждается в аргументах для создания. Поэтому контейнер разрешает его, возвращает и все вызовы проксируются на него.
Вспомним о фасадах в реальном времени: Мы присвоили нашему классу префикс Facades
. Во время загрузки приложения Laravel регистрирует spl_autoload_register()
, срабатывающий, при вызове неопределённых классов. В конечном итоге это приводит к методу load()
. Внутри load()
мы проверяем, имеет ли текущий неопределённый класс префикс Facades
. Если совпадает, то Laravel пытается загрузить его. Поскольку фасад не существует, он создаёт его из заглушки, а затем требует файл. И вуаля! У вас есть обычный фасад, но этот был создан на лету. Довольно круто, не так ли?
Заключение
Поздравляю, что вы зашли так далеко! Я понимаю, что это может быть немного ошеломляющим. Не стесняйтесь возвращаться назад и перечитывать разделы, которые не совсем понятны для вас. Вам также может помочь обращение к вашей IDE. Но эй, больше никакой чёрной магии, вы должны чувствовать себя хорошо, по крайней мере, так я чувствовал себя в первый раз!
И помните, что в следующий раз, когда вы будете вызывать метод статически, это может оказаться не так.