Laravel Octane — загрузка приложения и обработка запросов

Настройка приложения под Laravel Octane

В этой статье мы разберемся как работает под капотом Octane, как загружается приложение и что происходит при обработке запросов. А также расскажем как правильно настроить ваше приложение для работы под новый пакет.

При стандартной настройке LEMP-стека для каждого нового запроса загружается Laravel-приложение, регистрируются привязки контейнера, создаются новые экземпляры, запускается мидлвары, вызываются маршруты, после чего генерируется ответ для отправки в браузер.

При запуске вашего приложения под Laravel Octane меняется одна важная вещь: теперь ваше приложение загружается только один раз во время запуска воркеров и этот экземпляр будет использоваться для всех запросов.

Чтобы понять, как это работает, давайте посмотрим, что происходит при запуске Octane воркера:

$app = require BASE_PATH . '/bootstrap/app.php'

$app->bootstrapWith([
    LoadEnvironmentVariables::class,
    LoadConfiguration::class,
    HandleExceptions::class,
    RegisterFacades::class,
    SetRequestForConsole::class,
    RegisterProviders::class,
    BootProviders::class,
]);

$app->loadDeferredProviders();

foreach ($app->make('config')->get('octane.warm') as $service) {
    $app->make($service);
}

Как видите, при запуске воркера создается экземпляр Laravel-приложения. Затем этот экземпляр загружается путем выполнения нескольких операций, таких как загрузка файлов конфигурации, регистрация фасадов и сервис-провайдеров.

В этот момент все методы register и boot сервис-провайдеров уже вызваны, что означает — все сервисы приложения теперь связаны с контейнером. Другими словами, контейнер теперь знает, как запустить все привязки вашего приложения. Если мы сейчас вызовем app('config'), то контейнер будет знать, где ваша конфигурация.

Вызов метода loadDeferredProviders() гарантирует, что все отложенные провайдеры также будут зарегистрированы и загружены. Поскольку приложение загружается только один раз, то отложенный провайдер не даст никаких преимуществ в производительности. Octane загрузит их все, поэтому все сервисы, которые они предоставляют, будут привязаны к контейнеру и могут быть запущены по любому запросу.

Привязки в контейнере могут быть зарегистрированы как синглтоны — то есть они будут созданы один раз на все время жизни приложения. Их экземпляры будут храниться в кэше контейнера и одни и те же экземпляры будут повторно использоваться в течение всего жизненного цикла приложения. Это увеличит производительность вашего приложения, так как не придется каждый раз создавать их, когда они будут нужны.

Особое внимание обращайте на синглтоны. Любое состояние, хранящееся в этих экземплярах, будет сохраняться до тех пор, пока работает Octane-сервер.

Экземпляры  создаются вызовом $app->resolve('singleton') или $app->make('singleton'). То есть если вы создается какие-либо синглтоны в методах boot или register ваших сервис-провайдеров, то он сохраняется.Если они создаются при обработке запроса, то не сохраняются. Вам не нужно об этом беспокоиться.

Если вы хотите создать какие-либо синглтоны во время запуска воркера и вы явно не создается их в своем сервис-провайдере, то в Octane есть возможность заранее прогреть массив сервисов. Именно это и делают последние строки в приведенном выше примере:

foreach ($app->make('config')->get('octane.warm') as $service) {
    $app->make($service);
}

Octane перебирает в цикле сервисы и создает их за вас. Таким образом, они будут сохраняться в памяти контейнера, пока работает Octane.

Обработка запросов

Теперь, когда у нас есть экземпляр приложения с сервисным контейнером, который знает, куда направить все наши привязки, плюс имеет прогретые синглтоны, мы готовы начать обрабатывать запросы.

Вот как Octane обрабатывает входящие запросы:

$server->on('request', function($request) use ($app){
    $sandbox = clone $app;

    Container::setInstance($sandbox);

    $sandbox->make('events')->dispatch(new RequestReceived);

    $response = $sandbox->make(Kernel::class)->handle($request);

    Container::setInstance($app);

    return $response;
});

Octane клонирует исходный экземпляр приложения и использует его для обработки входящего запроса. Наличие экземпляра в песочнице для каждого запроса позволяет Octane сбрасывать службы, которые были изменены во время обработки запроса, поэтому следующий запрос получит чистое состояние.

Как говорилось ранее, синглтоны, работающие всё время жизни Octane-сервера, могут быть полезны для повышения производительности. Но нельзя допускать их изменений между запросами. Например, сервис config может вызывать нечто типа такого:

app('config')->set('services.aws.key', AWS_KEY_HERE);

Теперь при обработке следующего запроса ключ services.aws.key будет содержать значение, установленное предыдущим запросом. Отслеживание этих изменений может быть очень сложным, особенно если они были произведены сторонними пакетами. По этой причине Laravel предоставляет песочнице отдельный сервис config, который удаляется после каждого запроса, при этом исходный конфиг остается неизменным внутри исходного экземпляра приложения.

$sandbox->instance('config', clone $sandbox['config']);

В приведенном выше коде показано, как Octane предоставляет каждой песочнице клонированный экземпляр исходного конфига.

Теперь вернемся к тому, как Octane обрабатывает запросы. Прежде, чем экземпляр песочницы будет использован для обработки запроса, Octane отправляет событие RequestReceived (получен запрос). Он слушает это событие и выполняет несколько шагов для подготовки экземпляра песочницы для обработки запроса.

Вот некоторые из важных вещей, которые Octane делает при получении события RequestReceived:

$sandbox->instance('config', clone $sandbox['config']);

$sandbox[Kernel::class]->setApplication($sandbox);

$sandbox['cache']->store('array')->flush();

$sandbox['session']->driver()->flush();
$sandbox['session']->driver()->regenerate();

$sandbox['translator']->setLocale($sandbox['config']['app.locale']);
$sandbox['translator']->setFallback($sandbox['config']['app.fallback_locale']);

$sandbox['auth']->forgetGuards();

$app->instance('request', $request);
$sandbox->instance('request', $request);

Во-первых, он клонирует чистую копию репозитория конфигурации из только что клонированного нового экземпляра песочницы. Затем он связывает этот репозиторий с приложением-песочницей. Отныне любые изменения в конфигурациях должны затрагивать только песочницу.

Далее Octane передет экземпляр песочницы некоторым сервис-контейнерам. Таким образом, когда вызывается $this->app внутри этих сервисов, то будет возвращаться экземпляр песочницы, а не исходный экземпляр приложения. Octane делает это с несколькими сервисами, но мы в примере показываем только сервис Kernel.

Затем Octane сбрасывает кэш и сессии, чтобы они не сохранялись между запросами. По той же причине он также устанавливает дефолтные локали.

После этого Octane удаляет все экземпляры аутентификационных гвардов. Это очень важно, поскольку экземпляры гвардов кэширут внутри себя аутентифицированного пользователя, а нам нужно устанавливать пользователя для каждого запроса, ведь они могут быть разные.

Наконец, Octane передает экземпляр входящего запроса в исходный экземпляр приложения и в экземпляр песочницы. Причина передачи запроса исходному экземпляру приложения заключается в том, что мы хотим, чтобы любые сервисы, содержащие ссылку на исходный экземпляр приложения, по-прежнему могли получить доступ к запросу. Даже несмотря на то, что Octane не приветствует передачу ссылок на экземпляр приложения, мы все равно передаем запрос для обработки случаев, когда это происходит, до тех пор, пока все приложения и пакеты не смогут адаптировать свой код.

Нюансы настройки приложений

Теперь, когда мы понимаем, как Octane загружает наше приложение и использует его для обработки нескольких входящих запросов, я хочу выделить несколько моментов для разработчиков приложений и мейнтейнеров пакетов.

Передача экземпляра приложения в сервисы

Если сервису необходимо взаимодействовать с экземпляром приложения, и вы передаете его в конструктор, убедитесь, что вы используете экземпляр приложения, переданный в колбэк, а не переданный в сервис-провайдер:

// Вместо этого...
$this->app->bind(Service::class, function () {
    return new Service($this->app);
});

// сделайте так...
$this->app->bind(Service::class, function ($app) {
    return new Service($app);
});

Причина в том, что $this->app содержит ссылку на исходный экземпляр приложения, так как именно он использовался для регистрации провайдеров при загрузке.

Другой вариант — не передавать экземпляр приложения в конструктор, а вместо этого использовать хелперы app() или Container::getInstance(). Эти хелперы всегда будут ссылаться на экземпляр песочницы.

Передача экземпляра приложения в синглтоны

Не передавайте экземпляр приложения в синглтон, вместо этого передайте колбэк, который возвращает экземпляр песочницы.

// Вместо этого...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app);
});

// сделайте так...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn () => Container::getInstance());
});

Синглтоны живут между запросами, это означает, что экземпляр приложения, переданный при запуске синглтона в первый раз, будет использоваться для каждого запроса.

Передача экземпляра запроса в синглтоны

Как и в случае с экземпляром приложения, вместо этого передайте колбэк:

// Вместо этого...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app['request']);
});

// сделайте так...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn () => Container::getInstance()['request']);
});

Или используйте хелпер request() внутри сервиса и не вставляйте запрос как зависимость.

Передача экземпляра репозитория конфигурации в синглтоны

То же самое, что и в двух приведенных выше случаях — передавайте клбэк или используйте хелпер config():

// Вместо этого...
$this->app->singleton(Service::class, function ($app) {
    return new Service($app['config']);
});

// сделайте так...
$this->app->singleton(Service::class, function ($app) {
    return new Service(fn() => Container::getInstance()['config']);
});

Сохранение синглтонов

Между запросами будут сохраняться только синглтоны, запущенные во время начальной загрузки приложения. Синглтоны, созданные во время обработки запроса, будут зарегистрированы в контейнере песочницы, а он уничтожается после обработки запроса.

Чтобы сохранить синглтоны между запросами, вы можете либо запустить их в своих сервис-провайдерах, либо добавить их в раздел warm файла конфигурации Octane:

'warm' => [
    ...Octane::defaultServicesToWarm(),
    Service::class
],

С другой стороны, если у вас есть пакет, который регистрирует и запускает синглтон внутри сервис-провайдера и вы хотите сбрасывать этот экземпляр перед каждым запросом, то добавьте его в раздел flush файла конфигурации:

'flush' => [
    Service::class
],

Octane удаляет эти синглтоны из контейнера после обработки каждого запроса.

Автор: Mohamed Said
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.