Адаптеры рантаймов
Lift поставляет три необязательных адаптера для долгоживущих рантаймов PHP. Все они держат экземпляр $app живым между запросами, так что стоимость загрузки оплачивается только один раз — соединения с БД остаются прогретыми, кэш рефлексии горячий, а синглтоны переиспользуются.
| Рантайм | Класс | Транспорт |
|---|---|---|
| RoadRunner | Lift\Runtime\RoadRunnerWorker |
Go-процесс, PSR-7 через IPC |
| Swoole / OpenSwoole | Lift\Runtime\SwooleServer |
Расширение PHP, асинхронный ввод-вывод |
| FrankenPHP | Lift\Runtime\FrankenPhpWorker |
Встроенный Caddy, заполняет суперглобали |
Персистентное состояние — применимо ко всем рантаймам
Поскольку один и тот же процесс PHP обрабатывает много запросов, объекты-синглтоны, зарегистрированные через $app->singleton(), живут весь срок жизни воркера — это сделано намеренно:
- Соединения с базой данных сохраняются → поведение пула соединений, без накладных расходов на переподключение.
- Логгеры, кэши, HTTP-клиенты → прогреты и переиспользуются.
Состояние уровня запроса (например, аутентифицированный пользователь) никогда не должно храниться в синглтоне. Кладите его в атрибуты запроса:
// middleware
$user = Auth::check($request);
$request = $request->withAttribute('user', $user);
// обработчик
$user = $request->getAttribute('user');
RoadRunner
RoadRunner — это сервер PHP-приложений на основе Go. Воркеры — это долгоживущие процессы PHP, которые общаются с родительским Go-процессом через IPC.
Требования
composer require spiral/roadrunner-http nyholm/psr7
./vendor/bin/rr get-binary # скачивает бинарник rr
Настройка
worker.php (корень проекта):
<?php
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
(new \Lift\Runtime\RoadRunnerWorker($app))->serve();
.rr.yaml:
version: "3"
server:
command: "php worker.php"
http:
address: "0.0.0.0:8080"
pool:
num_workers: 4
max_jobs: 1000 # перезапускать воркер после N запросов (защита от утечек памяти)
Запуск:
./rr serve
Фабрика PSR-17
RoadRunnerWorker::serve() автоматически определяет фабрику PSR-17 из установленных пакетов (Nyholm → Guzzle → Laminas в таком порядке). Передайте свою, чтобы переопределить:
(new RoadRunnerWorker($app))->serve(new \Nyholm\Psr7\Factory\Psr17Factory());
Как это работает
Каждая итерация цикла:
PSR7Worker::waitRequest()блокируется, пока RoadRunner не доставит следующий HTTP-запрос как PSR-7ServerRequestInterface.Request::fromPsr7()преобразует его в LiftRequest.$app->handle($request)выполняет конвейер middleware + маршрутизатора.PSR7Worker::respond()отправляет LiftResponse(который уже реализуетResponseInterface) обратно в RoadRunner.- RoadRunner проксирует его клиенту.
Swoole / OpenSwoole
Swoole — это расширение PHP, добавляющее асинхронный, событийно-управляемый HTTP-сервер прямо в PHP. Внешний бинарник не требуется.
Требования
pecl install swoole
# или
pecl install openswoole
Включите в php.ini:
extension=swoole
Настройка
server.php (корень проекта):
<?php
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
(new \Lift\Runtime\SwooleServer($app))->start();
Запуск:
php server.php
Конфигурация
Передайте массив настроек Swoole вторым аргументом:
new \Lift\Runtime\SwooleServer($app, [
'host' => '0.0.0.0',
'port' => 9501,
'worker_num' => swoole_cpu_num() * 2,
'max_request' => 1000, // перезапускать воркер после N запросов
'daemonize' => false,
'log_file' => '/var/log/swoole.log',
]);
Полный список настроек: документация Swoole.
Корутины
Если вы включаете корутины (например, \Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL)), каждый обработчик запроса выполняется в собственной корутине. Стандартный PDO и блокирующий ввод-вывод не являются корутинно-осведомлёнными. Варианты:
- Используйте
Swoole\Database\PDOPoolдля корутинно-безопасного доступа к базе данных. - Или держите корутины отключёнными (по умолчанию) и полагайтесь на несколько воркеров для конкурентности.
Как это работает
Колбэк on('request', ...) срабатывает синхронно для каждого запроса:
SwooleServerпреобразует\Swoole\Http\Request→ LiftRequest(метод, URI, заголовки, cookie, тело).$app->handle($request)выполняет конвейер.- Статус, заголовки и тело записываются обратно в
\Swoole\Http\Response.
FrankenPHP
FrankenPHP — это сервер PHP-приложений, встроенный в Caddy. В режиме воркера он заполняет суперглобали ($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES) заново для каждого запроса — ровно как PHP-FPM. Это значит, что Request::fromGlobals() работает без изменений.
Требования
Скачайте бинарник FrankenPHP (он включает PHP + Caddy — отдельная установка не нужна):
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 \
-o frankenphp && chmod +x frankenphp
Настройка
worker.php (корень проекта):
<?php
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
(new \Lift\Runtime\FrankenPhpWorker($app))->serve();
Caddyfile:
{
frankenphp
admin off
auto_https off
}
:8080 {
root * public
# Маршрутизировать каждый запрос через worker.php.
# В режиме воркера Caddy использует этот путь для идентификации пула воркеров;
# уже запущенный воркер обрабатывает фактическую логику запроса.
rewrite * /worker.php
php_server {
worker worker.php 4 # количество воркеров; опустите, чтобы использовать число CPU
}
}
Запуск:
./frankenphp run --config Caddyfile
Как это работает
FrankenPhpWorker::serve() циклится на frankenphp_handle_request():
- FrankenPHP заполняет суперглобали и вызывает колбэк.
Request::fromGlobals()строит свежий LiftRequest.$app->handle($request)выполняет конвейер.- Ответ отправляется через
http_response_code(),header()иecho. - FrankenPHP завершает HTTP-цикл, и цикл продолжается.
Миграция с php-fpm
Если ваш существующий public/index.php вызывает $app->run(), оберните это в режим воркера FrankenPHP:
// worker.php
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
(new \Lift\Runtime\FrankenPhpWorker($app))->serve();
Ваш public/index.php может оставаться неизменным для традиционных развёртываний FPM. Для режима воркера FrankenPHP нужна только точка входа worker.php.