Skip to content

Symfony ​

Symfony поддерживает постоянный воркер. Приложение инициализирует ядро, передаёт ему Request и получает Response. Rapira инициализирует ядро один раз для каждого воркера. Затем каждый запрос вызывает handle() для того же ядра.

Код приложения не изменяется. Скрипт воркера заменяет public/index.php. Эта страница описывает этот файл, сброс состояния запроса и значения .env.

Проверено на

  • PHP 8.5.8: NTS, embed SAPI
  • Rapira 0.8.0
  • Symfony 7.4 (symfony/framework-bundle v7.4.15), проверено в dev и prod
  • Symfony 8.1 (symfony/framework-bundle v8.1.2), проверено в dev

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

Поведение в режиме Worker ​

Ядро инициализируется вне цикла и остаётся до перезапуска скрипта воркера. Автозагрузчик, контейнер, маршрутизатор, диспетчер событий и соединения инициализируются один раз. Дополнительная информация находится в разделах Режим Worker и Режимы выполнения.

Для каждого запроса обработчик создаёт Request из суперглобальных переменных, которые заполняет Rapira. Затем он вызывает handle(), send() и terminate(). В конце он вызывает services_resetter, чтобы сбросить сервисы с состоянием. Передача ответа описана в разделе HTTP.

Сессии используют встроенные функции сессий PHP. Запрос, который использует сессию, вызывает session_start(), и ответ содержит куку сессии. Следующий запрос читает сохранённую сессию. Тесты подтвердили, что разные клиенты получают разные сессии.

Каждый процесс воркера содержит одно ядро. Воркеры не разделяют объекты приложения. Число воркеров и надзор за ними описаны в разделе Модель процессов.

Требования ​

Установите Rapira. Создайте или выберите приложение Symfony. Поместите скрипт воркера рядом с composer.json.

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

Базовое приложение требует расширения ctype и iconv. Оно также заменяет их полифилы на PHP, поэтому оба расширения должны быть встроенными. Системному PHP CLI они тоже нужны для проверки платформы Composer. Каждый релиз Rapira содержит оба расширения.

Полный список расширений находится на странице Установка. Включите оба расширения при компиляции PHP. См. Сборка из исходников.

Воркер также использует компонент symfony/dotenv из базового приложения. Удалите вызов Dotenv, если окружение развёртывания задаёт все переменные окружения. Затем удалите компонент, если другие входные скрипты его не используют. Воркер читает .env и создаёт ядро без symfony/runtime. Оставьте symfony/runtime, потому что bin/console и public/index.php используют его.

Скрипт воркера ​

Сохраните этот файл как worker.php в корне проекта. Тесты использовали его с обеими версиями Symfony:

php
<?php

declare(strict_types=1);

use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpFoundation\Request;

require __DIR__ . '/vendor/autoload.php';

// public/index.php uses symfony/runtime for this operation.
// The worker performs it once before the request loop.
(new Dotenv())->bootEnv(__DIR__ . '/.env');

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();
$container = $kernel->getContainer();

$handler = static function () use ($kernel, $container): void {
    $request = Request::createFromGlobals();

    try {
        $response = $kernel->handle($request);
        $response->send();
        $kernel->terminate($request, $response);
    } finally {
        // Symfony uses the same reset between Messenger messages.
        // Each service with the kernel.reset tag removes request state.
        // The finally block also resets state when send() or terminate() throws.
        if ($container->has('services_resetter')) {
            $container->get('services_resetter')->reset();
        }
    }
};

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

Большая часть операций использует стандартную инициализацию Symfony. Четыре части относятся только к этому воркеру:

(new Dotenv())->bootEnv(...). Стандартный public/index.php передаёт эту операцию symfony/runtime. Воркер читает .env один раз перед созданием ядра. Rapira сохраняет эти значения $_ENV между запросами.

Ядро инициализируется перед циклом. new Kernel(...), boot() и getContainer() выполняются при инициализации воркера. Ядро читает $_SERVER['APP_ENV'] при инициализации воркера. Каждый запрос использует один контейнер.

Проверка $container->has('services_resetter') перед get(). Идентификатор services_resetter публичен в обеих поддерживаемых версиях. Класс реализации использует разные пространства имён в версиях 7.4 и 8.1. Идентификатор сервиса устраняет условие для версии. Проверка has() предотвращает ошибку, если контейнер не определяет сервис.

Цикл и gc_collect_cycles(). \Rapira\handle_request() ждёт запрос, выполняет обработчик и возвращает true. При завершении воркера она возвращает false, и цикл заканчивается. Скрипт собирает циклы между запросами. Полный контракт описан в разделе Режим Worker.

Если services_resetter недостаточно, используйте $container->reset() или $kernel->reboot(null). Первый вариант удаляет все созданные сервисы. Второй вариант удаляет контейнер и создаёт новый.

После $kernel->reboot(null) получите новый контейнер через $kernel->getContainer(). Обработчик не должен использовать старый контейнер. Оба варианта удаляют кешированное состояние приложения. Используйте их для поиска утечки памяти, а не как стандартную настройку.

$_ENV и окружение процесса ​

Rapira сохраняет $_ENV до повторного запуска скрипта воркера. Она не пересоздаёт эту суперглобальную переменную для каждого запроса. Значения, которые bootEnv() загружает до цикла, доступны в последующих запросах. Это поведение также действует с variables_order = "GPCS" и auto_globals_jit = On.

До первого запроса $_SERVER содержит окружение процесса. Dotenv не заменяет переменную, которая уже есть в $_SERVER или $_ENV. Поэтому переменная окружения имеет приоритет над той же переменной в .env, в том числе с variables_order = "GPCS".

Например, добавьте usePutenv(), если код приложения должен читать значения Dotenv через getenv():

php
(new Dotenv())->usePutenv()->bootEnv(__DIR__ . '/.env');

usePutenv() записывает значения Dotenv в окружение процесса. Symfony %env(...)% может читать сохранённые значения $_ENV без этого вызова. Rapira запускает один интерпретатор NTS PHP в каждом процессе. PHP не вызывает putenv() из параллельных потоков.

В продакшене задайте переменные окружения через systemd, контейнер или оркестратор. Используйте .env только при разработке.

Запуск Rapira ​

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

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "worker.php"
mode = "worker"

Запустите Rapira:

bash
rapira serve rapira.toml

mode = "worker" выбирает режим Worker. rapira serve работает на переднем плане.

Откройте другой терминал. Отправьте запрос:

bash
curl -i http://127.0.0.1:8000/

Нажмите Ctrl-C в первом терминале, чтобы остановить Rapira.

Входной скрипт - worker.php, поэтому $_SERVER['SCRIPT_NAME'] содержит /worker.php. Symfony не находит это значение в начале URI. Затем Symfony задаёт базовый URL как "". getPathInfo() возвращает путь запроса, и маршрутизация работает правильно. generateUrl() создаёт пути без префикса /worker.php. Для этого не нужны переопределения $_SERVER или Request::setTrustedProxies().

Продакшен ​

Задайте APP_ENV=prod. Установите зависимости без пакетов для разработки. Создайте кеш перед запуском сервера. Тесты подтвердили, что php bin/console cache:warmup правильно инициализирует приложение. Эта команда также компилирует контейнер до первого запроса:

bash
composer install --no-dev --optimize-autoloader
APP_ENV=prod php bin/console cache:warmup

Проверьте DEFAULT_URI при настройке. Базовое приложение задаёт router.default_uri как %env(DEFAULT_URI)% в каждом окружении. Значение по умолчанию: http://localhost. Консольные команды и код отправки писем используют это значение для создания URL вне HTTP-запроса. Укажите в нём адрес приложения.

Используйте этот минимальный rapira.toml:

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "worker.php"
mode = "worker"
processes = 4
max_requests = 500
request_terminate_timeout_secs = 30

max_requests заменяет воркер после случайного числа запросов от этого значения до 1,5 этого значения. Он ограничивает влияние утечки памяти, но не исправляет её. request_terminate_timeout_secs останавливает воркер, когда один запрос выполняется дольше этого предела. Новый воркер снова инициализирует ядро. Относительный entrypoint использует каталог файла конфигурации как базовый. Все настройки описаны в разделе Конфигурация.

Запустите сервер с APP_ENV=prod:

bash
APP_ENV=prod rapira serve rapira.toml

После развёртывания отправьте SIGUSR2 мастер-процессу, чтобы заменить воркеры. С opcache.validate_timestamps = 0 вместо этого перезапустите Rapira. См. Запуск в продакшене.

Сброс состояния между запросами ​

services_resetter вызывает reset() для каждого сервиса с тегом kernel.reset. Набор сервисов с этим тегом зависит от установленных бандлов. Примеры включают буферизованные обработчики логов и сборщики отладочных данных. Эти сервисы сами регистрируют тег.

Он не сбрасывает статические свойства приложения, глобальные значения, реестры библиотек или постоянные изменения ini_set(). Это состояние остаётся в каждом постоянном воркере. Сбрасывайте его в коде приложения. Таблица времени жизни состояния находится на странице Фреймворки.

Тесты со сбросом показали стабильную память процесса во время 200 последовательных запросов в dev и prod. Если память растёт, код приложения или бандл может сохранять состояние запроса.

Работа после ответа ​

Вызовите rapira_finish_request() между $response->send() и $kernel->terminate(), чтобы отправить ответ до запуска слушателей после ответа. Воркер продолжает выполнять terminate() до возврата из обработчика. Это может уменьшить время ожидания клиента, но не добавляет параллельность.

Разработка ​

В режиме Worker каждый воркер инициализирует приложение один раз. Перезапускайте Rapira после каждого изменения кода, чтобы загрузить новый код PHP. Также можно использовать режим Classic при разработке. Режим Classic выполняет входной скрипт для каждого запроса. Переведите rapira.toml в режим Classic:

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "public/index.php"
mode = "classic"
bash
rapira serve rapira.toml

В режиме Classic то же приложение инициализируется для каждого запроса. Поэтому сохранённые изменения действуют сразу.

Ошибки и логи ​

Symfony обрабатывает непойманное исключение приложения и возвращает свой ответ 500. В dev он содержит страницу исключения, а в prod содержит общую страницу ошибки. Тот же воркер обрабатывает следующий запрос. Финальный сброс удаляет изменённое состояние сервисов после исключения.

Настроенный логгер Symfony управляет выводом исключения. Базовое приложение не содержит логгер. Rapira записывает в лог ошибки PHP, которые Symfony не обрабатывает. Настройка уровней описана в разделе Логирование.