Skip to content

Yii3 ​

Yii3 поддерживает постоянные процессы. Воркер может один раз инициализировать приложение и сбрасывать состояние запроса после каждого ответа. Официальный раннер yiisoft/yii-runner-roadrunner использует ту же схему. Эта страница описывает постоянный воркер, альтернативу на каждый запрос и результаты интеграционных тестов.

Проверено на

  • PHP 8.5.8: NTS, embed SAPI
  • Rapira 0.8.0
  • Шаблон yiisoft/app 1.4 с yii-runner-http 3.2.1 (router-fastroute 4.x)

Тесты запускали оба скрипта воркера с этим ПО. Тесты покрывали маршрутизацию, URL, тела запросов, сессии, загрузку файлов, ошибки и 200 последовательных запросов. Примеры на этой странице используют формат конфигурации v0.9.

Yii3 и режим Worker ​

Резидентный воркер использует два элемента публичного API:

  • ApplicationRunner::getContainer() возвращает контейнер приложения. Воркеру не нужен подкласс или доступ к закрытому состоянию.
  • Yiisoft\Di\StateResetter - сервис этого контейнера. Компоненты регистрируют функции сброса, и один вызов reset() запускает их все.

Сервис приложения с состоянием запроса также должен зарегистрировать функцию. Добавьте ключ 'reset' => function (): void { … } в его DI-описание. yiisoft/session и yiisoft/router используют тот же способ. Замыкание может сбросить закрытое состояние без создания нового объекта. Время жизни состояния описано в обзоре фреймворков и разделе Режим Worker.

Резидентная схема состоит из трёх шагов. Создайте раннер один раз. Запускайте его для каждого запроса. Сбрасывайте контейнер после каждого запроса.

Требования ​

  • Установите Rapira. См. Установку.
  • Создайте или выберите приложение на Yii3. Можно использовать новый проект yiisoft/app.

Скрипт воркера - единственный новый файл PHP. Поместите его в корень проекта рядом с composer.json. Раннер использует корень проекта как rootPath.

Установите PHP CLI для Composer. Rapira поставляет PHP как библиотеку, а не как команду php. Rapira не использует и не изменяет системный PHP CLI.

Резидентный воркер ​

Это рекомендуемый вариант. Сохраните его как worker.php в корне проекта:

php
<?php

declare(strict_types=1);

use App\Environment;
use Yiisoft\Di\StateResetter;
use Yiisoft\Yii\Runner\Http\HttpApplicationRunner;

require_once __DIR__ . '/src/bootstrap.php';

$runner = new HttpApplicationRunner(
    rootPath: __DIR__,
    debug: Environment::appDebug(),
    checkEvents: Environment::appDebug(),
    environment: Environment::appEnv(),
);
$container = $runner->getContainer();

$handler = static function () use ($runner, $container): void {
    try {
        $runner->run();
    } finally {
        // The worker continues after an error leaves run().
        // Reset state before the next request.
        $container->get(StateResetter::class)->reset();
    }
};

while (\Rapira\handle_request($handler)) {
    gc_collect_cycles();
}

Скрипт выполняет следующие операции:

src/bootstrap.php инициализирует шаблон. Он подключает автозагрузчик Composer, читает .env, если файл есть, и вызывает Environment::prepare(). Стандартный public/index.php выполняет те же операции перед использованием раннера.

Воркер создаёт раннер один раз. Он использует аргументы rootPath, debug, checkEvents и environment из public/index.php. Поэтому воркер инициализирует то же приложение.

Обработчик вызывает run() и затем reset() для каждого запроса. run() обрабатывает запрос так же, как входной скрипт. reset() запускает зарегистрированные функции сброса перед следующим запросом.

Использование памяти оставалось стабильным. Тесты не обнаружили значительного увеличения памяти процесса за 200 последовательных запросов.

Новый раннер для каждого запроса ​

Создавайте раннер внутри обработчика, чтобы избежать постоянного состояния контейнера. Тогда объекты приложения принадлежат одному запросу:

php
<?php

declare(strict_types=1);

use App\Environment;
use Yiisoft\Yii\Runner\Http\HttpApplicationRunner;

require_once __DIR__ . '/src/bootstrap.php';

$handler = static function (): void {
    // Create one runner for each request.
    // Use the same arguments as public/index.php.
    $runner = new HttpApplicationRunner(
        rootPath: __DIR__,
        debug: Environment::appDebug(),
        checkEvents: Environment::appDebug(),
        environment: Environment::appEnv(),
    );
    $runner->run();
};

while (\Rapira\handle_request($handler)) {
    gc_collect_cycles();
}

Каждый запрос создаёт новый контейнер, поэтому воркер не сбрасывает состояние контейнера. Статические свойства, глобальные переменные и состояние инициализации остаются в воркере. Код приложения должен сбрасывать это состояние. Тесты также подтвердили эту схему.

Контейнер инициализируется для каждого запроса. Это увеличивает время инициализации и создаёт объекты, которые PHP должен позже освободить. Память может расти, пока PHP не освободит несколько старых контейнеров вместе. Это циклическое поведение не обязательно является утечкой памяти. См. Память и перезапуск воркеров.

Задайте http.pool.max_requests для периодической замены воркеров. Этот ключ описан на странице Конфигурация.

Эта схема не является режимом Classic. Автозагрузчик, бутстрап шаблона и цикл запросов остаются резидентными в воркере. Только приложение создаётся заново для каждого запроса.

По умолчанию используйте постоянный раннер. Он соответствует архитектуре фреймворка, требует одного вызова сброса и сохранял стабильное использование памяти в тестах. Используйте раннер на каждый запрос, если порядок инициализации или подготовка запроса не позволяют выполнить полный сброс через StateResetter. Для перехода между схемами измените только скрипт воркера.

Запуск Rapira ​

Создайте rapira.toml рядом с worker.php:

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "worker.php"
mode = "worker"
bash
rapira serve rapira.toml

mode = "worker" включает режим Worker. Команда описана в разделе Командная строка.

Для продакшена используйте полный rapira.toml:

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "/srv/app/worker.php"
mode = "worker"
processes = 8
max_requests = 500
request_terminate_timeout_secs = 30

[log]
level = "info"
format = "json"

Каждый ключ, значение по умолчанию и ограничение описаны на странице Конфигурация. Конфигурация systemd и обратного прокси описана в разделе Запуск в продакшене.

Статические файлы ​

Шаблон хранит favicon.ico, robots.txt и опубликованные бандлы ресурсов в public/. Rapira передаёт каждый запрос входному скрипту, если middleware статических файлов не отвечает на него. Добавьте middleware в секцию [http] в rapira.toml:

toml
[http]
listen = "127.0.0.1:8000"
middleware = ["static"]

[http.static]
root = "public"

Список forbid по умолчанию запрещает доступ к файлам .php, поэтому middleware не обслуживает public/index.php. Ресурсы также может обслуживать CDN или обратный прокси. Правила обслуживания описаны в разделе Интеграция с фреймворками.

Результаты тестов ​

Тесты применили одни и те же проверки к обеим схемам с шаблоном yiisoft/app. Результаты:

Маршрутизация работает без переопределений в $_SERVER. Rapira задаёт для SCRIPT_NAME значение /worker.php, то есть имя входного скрипта. FastRoute находил вложенные пути со строкой запроса. Корневой путь возвращал домашнюю страницу шаблона. Неизвестный путь возвращал ответ 404 фреймворка. Тесты не изменяли SCRIPT_NAME, REQUEST_URI или DOCUMENT_ROOT.

Сгенерированные URL не содержат имя файла воркера. UrlGeneratorInterface::generate() возвращал обычные пути приложения.

Yii3 изолирует сессию каждого клиента. Один клиент сохранял свой счётчик между запросами. Второй клиент получил новую сессию. Схема с постоянным контейнером дала тот же результат.

CSRF-токены работают без изменений. CsrfTokenMiddleware из шаблона хранит токен в сессии, и тесты подтвердили один токен для каждого клиента. Каждый POST по-прежнему требует свой токен. Если режим Worker отклоняет POST, убедитесь, что форма отправляет токен. Не изменяйте скрипт воркера из-за этой ошибки.

Приложение получает данные форм, JSON-тела и загруженные файлы. $_POST содержал поля формы, а php://input содержал JSON-тело. Временный файл загрузки был доступен для чтения во время запроса. PSR-7 ServerRequest содержал все эти значения.

Исключение в действии возвращает 500, и воркер продолжает работу. ErrorCatcher создаёт ответ с ошибкой и записывает исключение в лог. Тот же воркер обрабатывает следующий запрос в обычном режиме. Ошибки, которые останавливают воркер, описаны в разделе Режим Worker.

Режим Classic как запасной вариант ​

Yii3 работает и с обычным входным скриптом. Переведите rapira.toml в режим Classic:

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "public/index.php"
mode = "classic"

Эта конфигурация использует стандартный код приложения без скрипта воркера. Каждый запрос получает новое состояние приложения. Подробности см. в разделе Режим Classic.

Оставьте public/index.php как второй входной скрипт. Его используют режим Classic и встроенный сервер PHP.

Какие части шаблона воркер не использует?

Шаблон передаёт temporaryErrorHandler с логгером StreamTarget. Также он загружает c3.php, если включён APP_C3. Проверенный воркер не использует обе части. Без этого обработчика HttpApplicationRunner::createTemporaryErrorHandler() создаёт ErrorHandler с NullLogger. Поэтому раннер не записывает в лог ошибки создания конфигурации и контейнера. Передайте обработчик из шаблона, чтобы записывать эти ошибки.

Читает ли резидентный раннер текущий запрос?

Да. run() не хранит запрос с момента создания раннера. Каждый вызов получает RequestFactory и создаёт PSR-7 ServerRequest из суперглобальных переменных и php://input. Rapira заполняет эти значения перед каждым вызовом обработчика. Каждый вызов также регистрирует обработчик ошибок, вызывает runBootstrap() и вызывает checkEvents(), если его флаг равен true. Тесты подтвердили эту последовательность при 200 вызовах. Контракт данных запроса описан в разделе Режим Worker.

Нужно ли изменять условие cli-server в public/index.php?

Нет. Это условие обслуживает статические файлы и изменяет SCRIPT_NAME для встроенного сервера PHP. Rapira не выполняет его, потому что PHP_SAPI равен fastcgi на PHP 8.4 и rapira на PHP 8.5. Имя SAPI описано на странице Установка.