Лекция
асинхронность на backend — это не просто “модно и быстро”, а способ разделить время ответа пользователю и время выполнения задачи. В классическом синхронном сценарии сервер обрабатывает запрос полностью и только потом отправляет ответ. Это означает, что пользователь ждет завершения всей логики, даже если большая ее часть не влияет на сам ответ.
Современные backend-приложения, включая проекты на Laravel, используют асинхронные подходы, чтобы:
Асинхронность — это не один инструмент, а набор архитектурных решений: очереди, фоновые процессы, стриминг ответов, события и отложенные действия после ответа.

Важно сразу убрать распространенное заблуждение:
асинхронность на backend ≠ обязательно параллельность или event loop как в Node.js.
На практике это означает:
Сервер может:
Пример:
Задача выполняется:
Это самый распространенный вид асинхронности в PHP/Laravel.
Задачи выполняются вне HTTP-запроса:
Это уже настоящая асинхронная архитектура, потому что:
Ответ не ждет завершения всей логики:
Примеры:
Система реагирует на события:
Это уже шаг к более сложной архитектуре (event-driven systems).
Ключевая идея
Асинхронность — это не про “быстрее выполняется”, а про:
пользователь не ждет то, что не обязан ждать
Когда асинхронность нужна
Используй ее, если есть:
Когда не нужна
Не стоит усложнять, если:
Асинхронность добавляет:
что реально можно сделать на чистом PHP, где ответ становится “асинхронным” для пользователя, и в чем ограничения CGI/FPM.
Основные варианты:
Сразу вернуть ответ, а работу продолжить после него
Это самый практичный вариант для веба на PHP-FPM: отдать клиенту "accepted" или JSON со статусом, а потом продолжить выполнение кода на сервере. Для FastCGI/FPM для этого есть fastcgi_finish_request(), после которого ответ пользователю уже завершен, а PHP-скрипт еще может выполнять логику дальше.
Пример:
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'accepted',
'message' => 'Task started'
]);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// После этого пользователь уже получил ответ,
// а сервер может продолжать работу
file_put_contents(__DIR__ . '/log.txt', date('c') . " heavy work started\n", FILE_APPEND);
sleep(5); // имитация тяжелой работы
file_put_contents(__DIR__ . '/log.txt', date('c') . " heavy work finished\n", FILE_APPEND);
Отдать ответ и запустить фоновую задачу отдельным процессом
Это тоже очень популярный путь. PHP-скрипт быстро отвечает, а потом через exec() / shell_exec() / proc_open() запускает отдельный CLI-скрипт в фоне. Это уже не “настоящая асинхронность” внутри одного процесса, а делегирование работы другому процессу.
Пример:
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 'accepted']);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
$php = PHP_BINARY;
$script = __DIR__ . '/worker.php';
// Linux/macOS
exec("$php " . escapeshellarg($script) . " > /dev/null 2>&1 &");
worker.php:
file_put_contents(__DIR__ . '/log.txt', date('c') . " worker start\n", FILE_APPEND);
sleep(10);
file_put_contents(__DIR__ . '/log.txt', date('c') . " worker done\n", FILE_APPEND);
Long polling
PHP может держать запрос открытым какое-то время и ждать событие, а потом вернуть ответ. Это не совсем асинхронность на сервере, но для клиента выглядит как “ответ пришел позже сам”. Подходит для чатов, уведомлений, простых live-обновлений.
Схема:
Минусы:
Streaming / chunked response
PHP может отправлять данные частями через echo + flush(). Это удобно, когда нужно не ждать конца работы, а постепенно слать прогресс. Но надо помнить, что фактическая отправка зависит от буферизации PHP, веб-сервера и FastCGI. В документации PHP отмечено, что обработка разрыва соединения и отправка данных зависят от реальной записи в соединение.
Упрощенный пример:
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-cache');
echo "Start\n";
flush();
for ($i = 1; $i <= 5; $i++) {
sleep(1);
echo "Step $i\n";
flush();
}
echo "Done\n";
flush();
Игнорировать разрыв клиента и завершать работу в том же запросе
Если важно, чтобы задача дошла до конца даже если пользователь закрыл вкладку, можно использовать ignore_user_abort(true). PHP по умолчанию обычно завершает скрипт при разрыве клиента, но это поведение можно изменить. Также можно отслеживать состояние соединения через connection_aborted() и connection_status().
Пример:
ignore_user_abort(true); set_time_limit(0); echo "Accepted"; flush(); // Дальше выполняем задачу даже если клиент ушел sleep(15); file_put_contents(__DIR__ . '/log.txt', "finished\n", FILE_APPEND);
Fork процесса через pcntl_fork()
Это уже ближе к реальной параллельности: родительский PHP-процесс может отделить дочерний процесс. Но это работает в Unix/Linux CLI-сценариях и обычно не является хорошей идеей в обычном веб-запросе под FPM/Apache. Функция pcntl_fork() официально существует в расширении Process Control.
Идея:
$pid = pcntl_fork();
if ($pid === -1) {
die('fork failed');
}
if ($pid) {
echo "Parent: response to user";
} else {
sleep(10);
file_put_contents(__DIR__ . '/log.txt', "child finished\n", FILE_APPEND);
exit;
}
Файловая/БД очередь + периодический worker на PHP CLI
Если нужен “чистый PHP”, но уже по-взрослому, делают так:
Это самый надежный вариант без сторонних брокеров.
В отличие от HTTP:
$host = '0.0.0.0'; $port = 8080; $server = stream_socket_server("tcp://{$host}:{$port}", $errno, $errstr); stream_set_blocking($server, false); $clients = []; $handshaked = []; while (true) { $read = $clients; $read[] = $server; // принять новых клиентов // прочитать входящие сообщения // отправить сообщения нужным клиентам }
Если нужна простая и рабочая схема:
Сравнение с другими подходами при использовании чистого пхп
| Подход | Тип асинхронности | Когда использовать |
|---|---|---|
| Queue (Laravel) | фоновая | тяжелые задачи |
terminate() |
после ответа | лог, метрики |
afterResponse() |
микро-задачи | короткие действия |
| Streaming | постепенный ответ | прогресс |
| WebSocket | real-time push | чат, live-данные |
В обычном PHP под веб-сервером “асинхронность” чаще всего не такая, как в Node.js event loop. Обычно это:
То есть для “чистого PHP” самый здравый путь обычно такой:
HTTP → сохранить задачу → сразу ответить → worker делает работу → клиент спрашивает статус.
Основные способы .
1. Очереди (Queue / Job) — главный и самый правильный способ
Laravel рекомендует выносить тяжелые задачи в очереди: отправку email, обработку CSV, генерацию отчетов, импорт, интеграции и другие длительные операции. Идея простая: HTTP-запрос быстро возвращает ответ, а job обрабатывается в фоне worker-процессом. Laravel поддерживает несколько драйверов очередей, включая Redis, SQS и database.
Пример:
// Controller
public function store(Request $request)
{
ProcessOrderJob::dispatch($request->all());
return response()->json([
'status' => 'accepted',
'message' => 'Order queued',
], 202);
}
use Illuminate\Contracts\Queue\ShouldQueue;
class ProcessOrderJob implements ShouldQueue
{
public function __construct(public array $payload) {}
public function handle(): void
{
// тяжелая логика
}
}
Это лучший вариант, когда задача может выполняться отдельно от запроса.
2. dispatchAfterResponse() — выполнить job после отправки ответа браузеру
Laravel умеет отложить dispatch job до момента, когда ответ уже отправлен пользователю. В документации это описано как подход для коротких задач, обычно около секунды, например отправки письма. Это не полноценная очередь в архитектурном смысле, а “сделать сразу после ответа текущим процессом”.
Пример:
public function register()
{
SendWelcomeEmail::dispatchAfterResponse();
return response()->json([
'status' => 'ok',
'message' => 'User created',
]);
}
Или closure:
dispatch(function () {
\Log::info('Executed after response');
})->afterResponse();
return response()->json(['status' => 'ok']);
Подходит для очень коротких пост-действий. Для тяжелой обработки лучше обычная очередь.
3. dispatchSync() / sync driver — не асинхронно, а наоборот синхронно
Это важно не перепутать: Laravel имеет dispatchSync(), но он выполняет задачу в текущем процессе, а не в фоне. Для queueable jobs это идет через sync queue. То есть внешне это job, но реальной асинхронности тут нет.
Пример:
ProcessOrderJob::dispatchSync($data);
Это полезно для единообразия API, но не для ускорения ответа.
4. Batch и chain — сложные фоновые сценарии
Laravel позволяет запускать не только одиночные jobs, но и цепочки (chain) и пакеты (batch). Это удобно, когда асинхронная обработка состоит из нескольких этапов: импорт → обработка → уведомление. У batch также есть dispatch после ответа.
Пример идеи:
Bus::chain([ new PrepareImportJob($fileId), new ParseImportJob($fileId), new NotifyUserJob($userId), ])->dispatch();
Это уже “асинхронный pipeline”.
5. Streamed response — отдавать ответ частями
Laravel умеет формировать streamed response через response()->stream(...). Это другой тип “асинхронности”: пользователь не ждет, пока сформируется весь ответ, а получает его частями. Подходит для прогресса, больших текстов, логов, долгого экспорта, генерации больших файлов.
Пример:
public function stream()
{
return response()->stream(function () {
echo "Start\n";
ob_flush();
flush();
for ($i = 1; $i <= 5; $i++) {
sleep(1);
echo "Step {$i}\n";
ob_flush();
flush();
}
echo "Done\n";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/plain',
'Cache-Control' => 'no-cache',
]);
}
Это не очередь, а именно потоковая выдача ответа.
6. SSE / eventStream() — сервер отправляет события по мере появления
Laravel поддерживает Server-Sent Events через response()->eventStream(...). Это уже удобный встроенный способ делать “живые” ответы: прогресс, токены LLM, уведомления, состояние задачи. SSE работает как HTTP-поток с text/event-stream.
Пример:
public function events()
{
return response()->eventStream(function () {
yield "Step 1";
sleep(1);
yield "Step 2";
sleep(1);
yield "Finished";
});
}
Это хороший вариант, если надо именно “ответ приходит постепенно сам”.
7. streamJson() — потоковая выдача JSON
Laravel также поддерживает streamed JSON response. Это полезно, если нужно постепенно отдавать большой JSON без полного формирования в памяти.
8. streamDownload() — асинхронно для пользователя в смысле поэтапной загрузки
Если задача связана с экспортом, Laravel умеет делать потоковую загрузку файла. Это не фоновая очередь, но позволяет пользователю начать получать файл сразу, а не ждать полной генерации.
terminate() подходит, когда нужно:
Типичные кейсы:
Laravel прямо приводит пример, что middleware может делать работу после отправки ответа, а встроенный session middleware сохраняет сессию именно после ответа.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class LogAfterResponseMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// обычная логика до/во время запроса
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
\Log::info('Response already sent', [
'path' => $request->path(),
'status' => $response->getStatusCode(),
]);
}
}
Потом middleware подключается как обычный middleware — глобально или на маршрут. Исторически документация Laravel указывает, что terminable middleware нужно добавить в HTTP kernel, а сам факт “terminable” определяется просто наличием метода terminate.
Чем отличается terminate() middleware от dispatchAfterResponse()
Они похожи по смыслу: и там, и там работа идет после отправки ответа. Но роль разная:
Практически:
Что происходит под капотом terminate() middleware
Упрощенно цепочка такая:
Это видно и в Laravel API (Illuminate\Foundation\Http\Kernel::terminate и terminateMiddleware), и в Symfony docs: сначала $response->send(), потом $kernel->terminate(...), что и запускает поздние post-response действия.
То есть terminate():
Важная тонкость: это не тот же объект middleware
Laravel отдельно пишет, что при вызове terminate() middleware обычно резолвится заново из контейнера, то есть это может быть новый экземпляр, а не тот, что использовался в handle(). Если нужен один и тот же экземпляр для handle() и terminate(), middleware надо зарегистрировать как singleton.
Это очень важный момент. Например, так не стоит рассчитывать:
class BadMiddleware
{
private $startedAt;
public function handle($request, Closure $next)
{
$this->startedAt = microtime(true);
return $next($request);
}
public function terminate($request, $response)
{
// $this->startedAt может не сохраниться,
// потому что terminate может вызваться на новом экземпляре
}
}
Надежнее:
Потому что terminate() — это все еще часть жизненного цикла того же PHP-процесса запроса, просто после отправки ответа. Для действительно долгих задач Laravel queues подходят лучше, потому что они специально сделаны для deferred processing.
1. Через terminate() — лог и метрика
class ApiTimingMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$request->attributes->set('started_at', microtime(true));
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
$startedAt = $request->attributes->get('started_at');
$durationMs = $startedAt
? round((microtime(true) - $startedAt) * 1000, 2)
: null;
\Log::info('api_request_finished', [
'path' => $request->path(),
'status' => $response->getStatusCode(),
'duration_ms' => $durationMs,
]);
}
}
2. Через queue — тяжелая работа
public function store(Request $request)
{
ProcessImportJob::dispatch($request->all());
return response()->json([
'status' => 'accepted'
], 202);
}
3. Через dispatchAfterResponse() — маленькое пост-действие
public function submit()
{
dispatch(function () {
\Log::info('Light post-response action');
})->afterResponse();
return response()->json(['ok' => true]);
}
Асинхронность на backend — это архитектурный инструмент, позволяющий отделить пользовательский опыт от внутренней обработки данных. В PHP и Laravel она реализуется не через один механизм, а через комбинацию подходов: очереди, пост-обработку после ответа, стриминг и события.
Правильное использование асинхронности позволяет:
Но главное — понимать, что асинхронность — это не цель, а средство.
Ее стоит применять там, где она действительно решает проблему, а не просто “потому что можно”.
Комментарии
Оставить комментарий
Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)
Термины: Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)