Вы только что установили новое приложение 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
. Вот и все.
Поздравляем! Вы создали свой собственный фасад!
Теперь, когда вы хорошо разбираетесь в фасадах, можно раскрыть еще один волшебный трюк. Представьте себе, что вы можете вызвать 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. Но эй, никакой черной магии, должно быть, это хорошо, по крайней мере, так я чувствовал себя в первый раз!
И помните: в следующий раз, когда вы вызовете метод статически, это может оказаться не так 🪄