paint-brush
Laravel под капотом — что такое фасады?к@oussamamater
262 чтения

Laravel под капотом — что такое фасады?

к Oussama Mater14m2024/06/29
Read on Terminal Reader

Слишком долго; Читать

В состав Laravel входит множество фасадов, которые мы часто используем. Мы обсудим, что это такое, как мы можем создавать свои собственные Фасады, а также узнаем о Фасадах реального времени.
featured image - Laravel под капотом — что такое фасады?
Oussama Mater HackerNoon profile picture

Вы только что установили новое приложение Laravel, загрузили его и получили страницу приветствия. Как и все остальные, вы пытаетесь увидеть, как это отображается, поэтому заходите в файл web.php и встречаете этот код:

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

Понятно, как мы получили приветственное представление, но вам интересно, как работает маршрутизатор 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, // <- 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()); } } }

Когда поступает запрос, он отправляется на маршрутизатор. Непосредственно перед этим вызывается метод 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); // Interested here 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 , которое тестируем, внутриsolveFacadeInstance 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; // <- this guy 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

Теперь, когда вы хорошо разбираетесь в фасадах, можно раскрыть еще один волшебный трюк. Представьте себе, что вы можете вызвать HelloWorld::greet() без создания фасада, используя фасады реального времени .


Давайте посмотрим:

 <?php use Facades\App\Http\Controllers; // Notice the prefix 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(); // Interested here } }


В самом конце вы можете видеть, что вызывается метод 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


Логика проверяет, установлен ли $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. Но эй, никакой черной магии, должно быть, это хорошо, по крайней мере, так я чувствовал себя в первый раз!


И помните: в следующий раз, когда вы вызовете метод статически, это может оказаться не так 🪄