Сторонні пакети
Ми розглянули ядро Laravel і ваш власний код. Але список залежностей реального проєкту на цьому не закінчується: Debugbar під час розробки, Telescope для логів, Inertia для фронтенду, spatie/laravel-permission для ролей. Кожен із них написаний задовго до того, як хтось уявляв собі сотні конкурентних запитів усередині одного процесу, і кожен має власний синглтон, що несе стан. Добра новина: для найпоширеніших пакетів ця робота вже зроблена. Погана новина: не для всіх, і важливо вміти відрізнити одне від іншого.
Уже адаптовано
spatie/laravel-permission. PermissionRegistrar тримає поточний team ID та індекс wildcard-дозволів у власних властивостях, точно той патерн, що розглядався в розділі про небезпечні патерни. 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: сам список дозволів
// доступний лише для читання після завантаження і безпечно
// ділиться між запитами.
}У вашому власному коді нічого не змінюється: 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);
// Вони накопичують дані через кожну паузу вводу-виводу
// в запиті, тож сховище має бути на корутину, а не на екземпляр.
$this->messagesCollector = new AsyncMessagesCollector();
$this->timeCollector = new AsyncTimeDataCollector(...);
$this->exceptionsCollector = new AsyncExceptionsCollector();
}
}Різниця з ScopedServiceProxy з першого розділу важлива: там весь сервіс резолвився заново на кожен запит; тут сервіс лишається одним екземпляром (перестворювати його дорого: підписки на події, конфігурація), і контекстно-залежними стають лише конкретні акумулюючі колектори всередині нього. Той самий діагноз, що й у розділі про небезпечні патерни ("не пишіть у стан, спільний для запитів"), просто ліки, підібрані під конкретну анатомію цього пакета.
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, у них теж є синглтони, але те, що вони тримають, це конфігурація і клієнти, а не дані запиту. CacheManager кешує об'єкт клієнта Redis, а не значення, які ви туди кладете; MailManager кешує налаштований Mailer, а не листи. Те саме запитання з розділу про небезпечні патерни: чи переживає стан завершення запиту, лишаючись коректним? Тут відповідь так, тому що це конфігурація з'єднання, а не дані одного користувача.
Несумісні: вимикайте
livewire/livewire. Тут варто зупинитися і не переносити попередній оптимізм далі. LivewireManager накопичує стан, прив'язаний до запиту, глибоко всередині себе, а wire:stream побудований на припущеннях про буферизовану, одноразову відповідь, які сама модель конкурентних корутин ламає. Адаптувати його частинами не вийшло, ні для laravel-spawn, ні раніше для Laravel Octane. Єдина порада, яка насправді працює, не вмикати Livewire в асинхронному режимі взагалі. Для інтерактивних інтерфейсів на цьому стеку використовуйте Inertia, вона вже адаптована й розглянута вище. Filament, побудований поверх Livewire, успадковує те саме обмеження.
Рішення щодо власного пакета
Якщо потрібного вам пакета немає в жодному списку, питання те саме, що й у попередньому розділі, тільки тепер спрямоване на чужий код: чи тримає синглтон пакета мутабельний стан, коректний лише для одного запиту? Якщо так, є три шляхи, і вони не рівноцінні.
Найшвидший, це scoped_services у конфігу, "підхід перший": пакет резолвиться заново на кожен запит, а його власний код лишається незмінним.
// config/async.php
'scoped_services' => [
\SomePackage\Manager::class,
],Якщо перестворювати його дорого (пакет коштовний в ініціалізації або сам щось корисне кешує між запитами), напишіть цільовий адаптер за зразком AsyncPermissionRegistrar або AsyncDebugbar з цього розділу: успадкуйте клас, додайте bootCompleted(), перенесіть у current_context() лише те, що справді змінюється від запиту до запиту, решту лишіть як є.
А якщо це не окремий сервіс, а глибоко вкорінений патерн на кшталт Livewire, не витрачайте тиждень на адаптацію лише для того, щоб через тиждень виявити, що wire:stream зламаний з архітектурних причин. Запустіть MutableStaticPropertyRule із попереднього розділу проти вихідного коду пакета: якщо знахідок десятки і вони влучають у саме ядро пакета, а не в безпечні кеші часу завантаження, це сигнал виключити його з асинхронного режиму, а не виправляти.
Ми розглянули ваш власний код, ядро Laravel і його оточення. Лишилося побачити цифри: наскільки швидше насправді працює застосунок, коли все це вже на місці.