Сторонние пакеты
Ядро Laravel и собственный код разобрали. Но в реальном проекте на них список зависимостей не заканчивается: Debugbar в дев-окружении, Telescope для логов, Inertia для фронтенда, spatie/laravel-permission для ролей. Каждый из них написан задолго до того, как кто-то представил себе сотни конкурентных запросов в одном процессе, и у каждого свой собственный синглтон с состоянием. Хорошая новость: для самых частых пакетов эта работа уже сделана. Плохая: не для всех, и важно уметь отличить одно от другого.
Уже адаптированы
spatie/laravel-permission. PermissionRegistrar хранит текущий team ID и индекс wildcard-прав в собственных свойствах — ровно тот паттерн, что мы разбирали в главе про unsafe-паттерны. AsyncPermissionRegistrar переносит оба в current_context():
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 на воркер, но сами коллекторы делает контекстно-зависимыми:
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 в конфиге, «подход первый»: пакет резолвится заново на каждый запрос, код пакета не трогаем вообще.
// config/async.php
'scoped_services' => [
\SomePackage\Manager::class,
],Если пересоздание накладно (пакет дорого инициализируется или сам кэширует что-то полезное между запросами), пишется точечный адаптер по образцу AsyncPermissionRegistrar или AsyncDebugbar из этой главы: унаследовать класс, завести bootCompleted(), перенести в current_context() только то, что действительно меняется от запроса к запросу, остальное оставить как есть.
А если это не отдельный сервис, а глубоко въевшийся паттерн вроде Livewire — не тратьте на адаптацию неделю, чтобы через неделю обнаружить wire:stream сломанным по архитектурным причинам. Прогоните MutableStaticPropertyRule из прошлой главы по исходникам пакета: если находок десятки и они бьют в самое сердце пакета, а не в безопасные boot-time кэши, — это сигнал не чинить, а исключить из async-режима.
Свой код, ядро Laravel и его окружение разобрали. Осталось увидеть цифры: насколько в действительности быстрее становится приложение, когда всё это встаёт на свои места.