Вы только что установили новое приложение 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]; } } Сначала он проверяет, имеет ли статический массив значение с заданным (который, опять же, является ). Если он находит совпадение, он просто возвращает это значение. Это кэширование Laravel для небольшой оптимизации производительности. Это кэширование происходит в рамках одного запроса. Если этот метод вызывается несколько раз с одним и тем же аргументом в одном запросе, он использует кэшированное значение. Давайте предположим, что это первоначальный вызов, и продолжим. $resolvedInstance $name router Затем он проверяет, установлено ли и является ли экземпляром контейнера приложения. $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; } Видите ли, мы присваиваем свойству , которое тестируем, внутриsolveFacadeInstance . Это отвечает на вопрос; давай продолжим. $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]; } } Мы подтвердили, что устанавливается во время загрузки приложения. Следующий шаг — проверить, следует ли кэшировать разрешенный экземпляр, проверив , который по умолчанию имеет значение true. Наконец, мы извлекаем экземпляр из контейнера приложения. В нашем случае это похоже на запрос предоставить любой класс, привязанный к строке . $app $cached static::$app['router'] router Теперь вы можете задаться вопросом, почему мы обращаемся как к массиву, несмотря на то, что он является экземпляром контейнера приложения, то есть . Ну, ты прав! Однако контейнер приложения реализует интерфейс PHP под названием , обеспечивающий доступ, подобный массиву. Мы можем взглянуть на него, чтобы подтвердить этот факт: $app объектом ArrayAccess <?php namespace Illuminate\Container; use ArrayAccess; // <- this guy use Illuminate\Contracts\Container\Container as ContainerContract; class Container implements ArrayAccess, ContainerContract { // ... } Итак, действительно возвращает экземпляр, привязанный к строке , а именно . Откуда я узнал? Взгляните на фасад ; часто вы встретите PHPDoc , намекающий на то, что скрывается за этим фасадом или, точнее, на какой класс будут пересылаться вызовы наших методов. resolveFacadeInstance() router \Illuminate\Routing\Router Route @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 вызывает функцию , встроенную функцию PHP, которая срабатывает при попытке доступа к неопределенному классу. Он определяет логику выполнения в таких ситуациях. В этом случае Laravel решает вызвать метод при обнаружении неопределенного вызова. spl_autoload_register() 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 Повторение фасадов в реальном времени: мы добавили к нашему классу префикс . Во время загрузки приложения Laravel регистрирует , который срабатывает, когда мы вызываем неопределенные классы. В конечном итоге это приводит к методу . Внутри мы проверяем, имеет ли текущий неопределенный класс префикс . Он совпадает, поэтому Laravel пытается его загрузить. Facades spl_autoload_register() load() load() Facades Поскольку фасад не существует, он создает его из заглушки, а затем требует файл. И вуаля! У вас обычный фасад, а этот создан на лету. Довольно круто, да? Заключение Поздравляем с тем, что вы зашли так далеко! Я понимаю, что это может быть немного ошеломляющим. Не стесняйтесь вернуться и перечитать любые разделы, которые вам не совсем понравились. Также может помочь работа с вашей IDE. Но эй, никакой черной магии, должно быть, это хорошо, по крайней мере, так я чувствовал себя в первый раз! И помните: в следующий раз, когда вы вызовете метод статически, это может оказаться не так 🪄