Вам бонус- начислено 1 монета за дневную активность. Сейчас у вас 1 монета

Способы создания асинхронных ответов на чистом PHP или на Laravel (/backend)

Лекция



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

Современные backend-приложения, включая проекты на Laravel, используют асинхронные подходы, чтобы:

  • ускорить отклик API;
  • разгрузить веб-сервер;
  • повысить масштабируемость;
  • обрабатывать тяжелые задачи в фоне.

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

Способы создания асинхронных ответов на чистом PHP или на  Laravel (backend)

Что понимать под асинхронностью на backend

Важно сразу убрать распространенное заблуждение:
асинхронность на backend ≠ обязательно параллельность или event loop как в Node.js.

На практике это означает:

1. Разделение “ответа” и “работы”

Сервер может:

  • быстро вернуть HTTP-ответ (200, 202 Accepted);
  • продолжить выполнение задачи позже или в другом процессе.

Пример:

  • пользователь нажал “создать отчет”;
  • сервер сразу отвечает: “задача принята”;
  • отчет генерируется в фоне.

2. Отложенное выполнение (deferred execution)

Задача выполняется:

  • после отправки ответа (например, terminate() или afterResponse);
  • или в очереди (job/worker).

Это самый распространенный вид асинхронности в PHP/Laravel.

3. Фоновая обработка (background processing)

Задачи выполняются вне HTTP-запроса:

  • отдельные воркеры;
  • очереди (Redis, DB и т.д.).

Это уже настоящая асинхронная архитектура, потому что:

  • выполнение не связано с жизненным циклом запроса;
  • есть retry, устойчивость, масштабирование.

4. Потоковые ответы (streaming)

Ответ не ждет завершения всей логики:

  • данные отправляются частями;
  • пользователь получает прогресс в реальном времени.

Примеры:

  • лог выполнения;
  • генерация файла;
  • стриминг AI-ответа.

5. Событийная модель (event-driven)

Система реагирует на события:

  • “пользователь зарегистрирован” → отправить email;
  • “платеж завершен” → обновить статус.

Это уже шаг к более сложной архитектуре (event-driven systems).

Ключевая идея

Асинхронность — это не про “быстрее выполняется”, а про:

пользователь не ждет то, что не обязан ждать

Когда асинхронность нужна

Используй ее, если есть:

  • долгие операции (импорт, отчеты, API-интеграции);
  • высокая нагрузка;
  • UX-требования “быстрого ответа”;
  • задачи, не влияющие на immediate response.

Когда не нужна

Не стоит усложнять, если:

  • операция занимает миллисекунды;
  • нет нагрузки;
  • нет смысла разделять процесс.

Асинхронность добавляет:

  • сложность;
  • необходимость мониторинга;
  • отладку фоновых процессов.

что реально можно сделать на чистом PHP, где ответ становится “асинхронным” для пользователя, и в чем ограничения CGI/FPM.

На чистом PHP “асинхронный ответ” обычно делают не одним способом, а одним из нескольких паттернов — в зависимости от того, что именно нужно: быстро отдать HTTP-ответ, продолжить тяжелую работу после ответа, или реально выполнять задачи параллельно.

Основные варианты:

  1. Сразу вернуть ответ, а работу продолжить после него
    Это самый практичный вариант для веба на 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);
  2. Отдать ответ и запустить фоновую задачу отдельным процессом
    Это тоже очень популярный путь. 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);
  3. Long polling
    PHP может держать запрос открытым какое-то время и ждать событие, а потом вернуть ответ. Это не совсем асинхронность на сервере, но для клиента выглядит как “ответ пришел позже сам”. Подходит для чатов, уведомлений, простых live-обновлений.

    Схема:

    • клиент делает запрос;
    • PHP в цикле проверяет, появилось ли событие;
    • если появилось — сразу отвечает;
    • если нет — по таймауту возвращает пустой ответ.

    Минусы:

    • держит воркеры занятыми;
    • плохо масштабируется при большом числе клиентов.
  4. 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();
  5. Игнорировать разрыв клиента и завершать работу в том же запросе
    Если важно, чтобы задача дошла до конца даже если пользователь закрыл вкладку, можно использовать 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);
  6. 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;
    }
  7. Файловая/БД очередь + периодический worker на PHP CLI
    Если нужен “чистый PHP”, но уже по-взрослому, делают так:

    • HTTP-запрос кладет задачу в БД/файл;
    • сразу возвращает пользователю task_id;
    • отдельный PHP CLI worker периодически забирает задачи и выполняет их;
    • клиент спрашивает /status?id=....

    Это самый надежный вариант без сторонних брокеров.

8. WebSocket — это еще один важный способ “асинхронности” на backend, WebSocket — это постоянное двустороннее соединение между клиентом и сервером.

В отличие от HTTP:

  • HTTP → запрос → ответ → соединение закрыто
  • WebSocket → соединение открыто постоянно → сервер и клиент могут отправлять данные в любой момент

$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;
  // принять новых клиентов
  // прочитать входящие сообщения
  // отправить сообщения нужным клиентам
}

Что выбрать на практике

Если нужна простая и рабочая схема:

  • быстро ответить и доделать потом → fastcgi_finish_request()
  • долгая фоновая задача → отдельный CLI-процесс через exec()
  • проверка статуса выполнения → задача в БД + polling /status
  • передавать прогресс в реальном времени → streaming через flush()
  • реальная параллельность → pcntl_fork() только для CLI/Linux-сценариев

Сравнение с другими подходами при использовании чистого пхп

Подход Тип асинхронности Когда использовать
Queue (Laravel) фоновая тяжелые задачи
terminate() после ответа лог, метрики
afterResponse() микро-задачи короткие действия
Streaming постепенный ответ прогресс
WebSocket real-time push чат, live-данные

Важное ограничение

В обычном PHP под веб-сервером “асинхронность” чаще всего не такая, как в Node.js event loop. Обычно это:

  • либо ранний ответ + продолжение работы,
  • либо запуск отдельного процесса,
  • либо очередь + worker.

То есть для “чистого PHP” самый здравый путь обычно такой:
HTTP → сохранить задачу → сразу ответить → worker делает работу → клиент спрашивает статус.

Как это выглядит в Laravel (в контексте)

  • Queue / Job → настоящая асинхронность
  • dispatchAfterResponse() → короткие действия после ответа
  • middleware terminate() → инфраструктурная пост-обработка
  • stream() / eventStream() → постепенная отдача ответа
Во фреймворке Laravel “асинхронный ответ” обычно делают не одним способом, а через несколько встроенных подходов — в зависимости от цели: быстро вернуть HTTP-ответ, выполнить код сразу после отправки ответа, вынести работу в очередь, или стримить данные пользователю по мере готовности. Laravel официально поддерживает все эти сценарии.

Основные способы .

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 умеет делать потоковую загрузку файла. Это не фоновая очередь, но позволяет пользователю начать получать файл сразу, а не ждать полной генерации.

9 terminable middleware

В Laravel есть terminable middleware: middleware с методом terminate(), который вызывается после того, как HTTP-ответ уже отправлен клиенту. Это задокументировано в Laravel, а в основе механизма лежит вызов Kernel::terminate($request, $response) после send() ответа; похожий механизм есть и в Symfony через событие kernel.terminate.

Когда это использовать в том же контексте

terminate() подходит, когда нужно:

  • не задерживать ответ пользователю;
  • сделать короткое пост-действие сразу после ответа;
  • не поднимать отдельный queue worker ради мелочи.

Типичные кейсы:

  • дописать лог;
  • отправить метрику;
  • сохранить audit trail;
  • финализировать сессию;
  • легкое уведомление или вебхук, если это действительно быстро.

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 — это инфраструктурный слой запроса/ответа; хорошо для логирования, метрик, сессий, кросс-срезной логики.
  • dispatchAfterResponse() — это скорее прикладное действие уровня job/бизнес-операции. Laravel отдельно рекомендует такой подход для коротких задач, обычно порядка секунды, а для тяжелых операций — уже очередь.

Практически:

  • нужно “после каждого запроса что-то тихо делать” → terminate();
  • нужно “после конкретного controller action доделать мини-задачу” → afterResponse;
  • нужно что-то тяжелое и надежное → queue/job worker. Laravel queues предназначены именно для отложенной обработки долгих задач.

Что происходит под капотом terminate() middleware

Упрощенно цепочка такая:

  1. Laravel обрабатывает запрос.
  2. Формируется объект Response.
  3. Ответ отправляется клиенту.
  4. После этого вызывается Kernel::terminate($request, $response).
  5. Kernel проходит по middleware и вызывает terminate() у тех, у кого этот метод есть.

Это видно и в Laravel API (Illuminate\Foundation\Http\Kernel::terminate и terminateMiddleware), и в Symfony docs: сначала $response->send(), потом $kernel->terminate(...), что и запускает поздние post-response действия.

То есть terminate():

  • не часть основного ответа;
  • не меняет уже отправленный body/status/headers;
  • выполняется уже “после”.

Важная тонкость: это не тот же объект 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(),
  • либо хранить данные в request->attributes,
  • либо регистрировать middleware как singleton, если это действительно нужно.

Когда terminate() — плохая идея

Не стоит класть туда:

  • тяжелый импорт;
  • обработку больших файлов;
  • долгую интеграцию с внешними API;
  • критически важные задачи, где нужен retry и надежность.

Потому что 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]);
}

Простая классификация

  • Фоновая обработка → Queue / Job / Worker.
  • После ответа, но еще тем же приложением → dispatchAfterResponse().
  • Постепенная отдача ответа → stream().
  • Поток событий клиенту → eventStream() / SSE.
  • Большой JSON по частям → streamJson().
  • Большой файл по частям → streamDownload().

Что выбирать при использовании Laravel

  • Если задача тяжелая и может жить отдельно от запроса — обычная очередь + worker. Это основной production-подход в Laravel.
  • Если действие очень короткое и нужно лишь не задерживать ответ — dispatchAfterResponse(). Laravel сам отмечает, что это обычно для коротких задач, вроде отправки email.
  • Если нужно показывать прогресс или отдавать данные постепенно — stream() или eventStream(). Для событий в реальном времени по HTTP лучше SSE через eventStream().
  • terminate() → пост-логика уровня middleware: лог, метрика, сессия, audit.
  • afterResponse() → короткое прикладное действие после ответа.
  • Queue / Job → тяжелая, надежная, повторяемая фоновая обработка.
  • stream() / eventStream() → когда ответ нужно слать частями, а не просто “доделывать потом”. Это отдельный сценарий потоковых ответов.

Заключение

Асинхронность на backend — это архитектурный инструмент, позволяющий отделить пользовательский опыт от внутренней обработки данных. В PHP и Laravel она реализуется не через один механизм, а через комбинацию подходов: очереди, пост-обработку после ответа, стриминг и события.

Правильное использование асинхронности позволяет:

  • ускорить API;
  • повысить устойчивость системы;
  • масштабировать обработку задач.

Но главное — понимать, что асинхронность — это не цель, а средство.
Ее стоит применять там, где она действительно решает проблему, а не просто “потому что можно”.

создано: 2026-04-19
обновлено: 2026-04-22
1



Рейтиг 9 of 10. count vote: 2
Вы довольны ?:


Поделиться:
Пожаловаться

Найди готовое или заработай

С нашими удобными сервисами без комиссии*

Как это работает? | Узнать цену?

Найти исполнителя
$0 / весь год.
  • У вас есть задание, но нет времени его делать
  • Вы хотите найти профессионала для выплнения задания
  • Возможно примерение функции гаранта на сделку
  • Приорететная поддержка
  • идеально подходит для студентов, у которых нет времени для решения заданий
Готовое решение
$0 / весь год.
  • Вы можите продать(исполнителем) или купить(заказчиком) готовое решение
  • Вам предоставят готовое решение
  • Будет предоставлено в минимальные сроки т.к. задание уже готовое
  • Вы получите базовую гарантию 8 дней
  • Вы можете заработать на материалах
  • подходит как для студентов так и для преподавателей
Я исполнитель
$0 / весь год.
  • Вы профессионал своего дела
  • У вас есть опыт и желание зарабатывать
  • Вы хотите помочь в решении задач или написании работ
  • Возможно примерение функции гаранта на сделку
  • подходит для опытных студентов так и для преподавателей

Комментарии


Оставить комментарий
Если у вас есть какое-либо предложение, идея, благодарность или комментарий, не стесняйтесь писать. Мы очень ценим отзывы и рады услышать ваше мнение.
To reply

Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)

Термины: Выполнение скриптов на стороне сервера PHP (LAMP) NodeJS (Backend)