LaravelСторонние пакеты

Сторонние пакеты

Ядро Laravel и собственный код разобрали. Но в реальном проекте на них список зависимостей не заканчивается: Debugbar в дев-окружении, Telescope для логов, Inertia для фронтенда, spatie/laravel-permission для ролей. Каждый из них написан задолго до того, как кто-то представил себе сотни конкурентных запросов в одном процессе, и у каждого свой собственный синглтон с состоянием. Хорошая новость: для самых частых пакетов эта работа уже сделана. Плохая: не для всех, и важно уметь отличить одно от другого.

Уже адаптированы

spatie/laravel-permission. PermissionRegistrar хранит текущий team ID и индекс wildcard-прав в собственных свойствах — ровно тот паттерн, что мы разбирали в главе про unsafe-паттерны. AsyncPermissionRegistrar переносит оба в current_context():

php
class AsyncPermissionRegistrar extends PermissionRegistrar
{
    public function setPermissionsTeamId(int|string|Model|null $id): void
    {
        current_context()->set(self::CTX_TEAM_ID, $id, replace: true);
    }

    public function getPermissionsTeamId(): int|string|null
    {
        return current_context()->find(self::CTX_TEAM_ID);
    }

    // clearPermissionsCollection() становится no-op: сам список прав
    // read-only после загрузки и безопасно шарится между запросами.
}

Ничего не меняется в вашем коде: Auth::user()->can(...), setPermissionsTeamId() в middleware — работают как в документации пакета.

inertiajs/inertia-laravel. AsyncResponseFactory переносит в контекст sharedProps, rootView, version, encryptHistory — всё, что раньше накапливалось в свойствах синглтона ResponseFactory и переживало бы конец запроса, попадая в следующий.

barryvdh/laravel-debugbar. Здесь решение чуть тоньше. Debugbar собирает данные весь запрос: SQL-запросы, сообщения, тайминги — классический аккумулирующий коллектор, который пишет в себя на протяжении всего цикла обработки, включая паузы на await. AsyncDebugbar не резолвит новый экземпляр на каждый запрос (это сломало бы одноразовые подписки на события), а держит один Debugbar на воркер, но сами коллекторы делает контекстно-зависимыми:

php
class AsyncDebugbar extends LaravelDebugbar
{
    public function __construct(Application $app, Request $request)
    {
        parent::__construct($app, $request);

        // Аккумулируют данные через все паузы запроса на I/O — значит
        // хранилище должно быть per-coroutine, а не per-instance.
        $this->messagesCollector   = new AsyncMessagesCollector();
        $this->timeCollector       = new AsyncTimeDataCollector(...);
        $this->exceptionsCollector = new AsyncExceptionsCollector();
    }
}

Разница с ScopedServiceProxy из первой главы принципиальна: там весь сервис резолвился заново на запрос, здесь сервис остаётся один (его дорого пересоздавать — подписки на события, конфигурация), а контекстно-зависимыми становятся только конкретные накопительные коллекторы внутри него. Тот же диагноз, что и в главе про unsafe-паттерны («не пишите в разделяемое между запросами состояние»), просто лечение подобрано под конкретную анатомию пакета.

laravel/telescope. Аналогично: entriesQueue, updatesQueue, флаг shouldRecord уходят в контекст через трейт CoroutineSafeRecording, а решение о том, писать ли конкретный запрос в лог, принимается для каждой корутины отдельно.

laravel/socialite. Тут проще: SocialiteManager кэширует драйверы вместе с конфигом первого же запроса, который до них дотянулся. Решение — не адаптер, а scopedSingleton, тот самый «подход первый» из первой главы: свежий менеджер на запрос, никакого контекста не нужно.

Безопасны без адаптации

Cache, Queue, Mail, Log, Validation, Filesystem, HTTP Client, Notifications, Encryption, Hashing, Pagination, Sanctum, Passport, Scout, Cashier, Horizon — синглтоны у них тоже есть, но хранят они конфигурацию и клиентов, а не per-request данные. CacheManager кэширует объект Redis-клиента, а не значения, которые вы туда положили; MailManager кэширует настроенный Mailer, а не письма. Тот же вопрос из главы про unsafe-паттерны: переживает ли состояние конец запроса, оставаясь верным? Здесь ответ да, потому что состояние — это конфигурация подключения, а не данные одного пользователя.

Несовместимы — выключать

livewire/livewire. Вот здесь стоит остановиться и не переносить предыдущий оптимизм. LivewireManager копит per-request состояние глубоко внутри себя, wire:stream завязан на предположения о буферизованном, одноразовом ответе, которые ломает сама модель конкурентных корутин. Адаптировать частями — не вышло ни у laravel-spawn, ни у Laravel Octane до него. Единственная рабочая рекомендация — не включать Livewire в async-режиме вообще. Для интерактивных интерфейсов на этом стеке используйте Inertia — она уже адаптирована и описана выше. Filament, построенный поверх Livewire, наследует то же ограничение.

Как решать по своему пакету

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

Быстрее всего — scoped_services в конфиге, «подход первый»: пакет резолвится заново на каждый запрос, код пакета не трогаем вообще.

php
// config/async.php
'scoped_services' => [
    \SomePackage\Manager::class,
],

Если пересоздание накладно (пакет дорого инициализируется или сам кэширует что-то полезное между запросами), пишется точечный адаптер по образцу AsyncPermissionRegistrar или AsyncDebugbar из этой главы: унаследовать класс, завести bootCompleted(), перенести в current_context() только то, что действительно меняется от запроса к запросу, остальное оставить как есть.

А если это не отдельный сервис, а глубоко въевшийся паттерн вроде Livewire — не тратьте на адаптацию неделю, чтобы через неделю обнаружить wire:stream сломанным по архитектурным причинам. Прогоните MutableStaticPropertyRule из прошлой главы по исходникам пакета: если находок десятки и они бьют в самое сердце пакета, а не в безопасные boot-time кэши, — это сигнал не чинить, а исключить из async-режима.

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