Архитектура
Как загружается приложение Lift, как запрос проходит сквозь него и как контейнер разрешает зависимости. Никаких сюрпризов — всё явно.
Жизненный цикл запроса
С момента, когда PHP получает HTTP-запрос, до момента, когда отправлен ответ.
Конвейер middleware
Луковичная модель PSR-15 — каждый слой оборачивает следующий. Самый внутренний слой — обработчик маршрута.
┌─ GlobalMiddleware A ────────────────────┐ │ ┌─ GlobalMiddleware B ───────────────┐ │ │ │ ┌─ GroupMiddleware ─────────────┐ │ │ │ │ │ ┌─ RouteMiddleware ────────┐ │ │ │ │ │ │ │ ┌─ Handler ──────────┐ │ │ │ │ │ │ │ │ │ return $response │ │ │ │ │ │ │ │ │ └────────────────────── ┘ │ │ │ │ │ │ │ └────────────────────────────┘ │ │ │ │ │ └──────────────────────────────── ┘ │ │ │ └────────────────────────────────────── ┘ │ └────────────────────────────────────────────┘
// 1. Глобальный — каждый запрос
$app->use(LoggingMiddleware::class);
$app->use(CorsMiddleware::class);
// 2. Группа — только /api/*
$app->group('/api', function ($g) {
$g->middleware(AuthMiddleware::class);
// 3. Маршрут — только этот эндпоинт
$g->get('/admin', AdminHandler::class)
->middleware(RequireAdminMiddleware::class);
});
MiddlewareInterface::process(ServerRequestInterface, RequestHandlerInterface).
Он может прервать цепочку, вернув ответ напрямую — внутренние слои тогда не вызываются.
Middleware аутентификации и ограничения частоты используют это, чтобы отклонять неаутентифицированные запросы до запуска любого обработчика.
Устройство маршрутизатора
Двухуровневая диспетчеризация: хеш-карта за O(1) для статических маршрутов и просмотр скомпилированных regex по каждому маршруту для динамических.
GET /,
POST /auth/login —
маршруты без параметров хранятся во вложенном массиве по ключу [method][path].
Разрешение это одно чтение массива: O(1), без сканирования строк.
// поиск: isset($staticRoutes[$method][$path])
Маршруты с параметрами (/users/{id:\d+}) каждый компилирует свой regex один раз
— кэшируется на объекте маршрута, не перестраивается на каждый запрос. Диспетчеризация просматривает динамические маршруты по порядку, проверяя каждый скомпилированный regex, пока один не совпадёт.
~^/users/(?<id>\d+)$~
{id:\d+}) запекаются в скомпилированный regex —
маршрут совпадает только когда ограничение проходит, поэтому дополнительный шаг валидации в вашем обработчике не нужен.
DI-контейнер
Контейнер PSR-11 с автосвязыванием конструктора. Три режима регистрации, один алгоритм разрешения.
Фабрика вызывается каждый раз при вызове make(). Используйте для безсостоятельных сервисов.
$app->bind(Mailer::class, fn() => new Mailer($_ENV));
Фабрика вызывается один раз; экземпляр кэшируется и переиспользуется при каждом последующем make().
$app->singleton(DB::class, fn() => new DB($_ENV['DSN']));
Хранит уже построенный объект напрямую. Полезно, когда вы конструируете значение до его регистрации.
$app->instance(Config::class, $config);
Алгоритм автосвязывания
Когда make(ClassName::class) вызывается без зарегистрированной привязки:
Загрузка
Что происходит между new App() и обслуживанием первого запроса.
Создаёт пустой контейнер PSR-11 и пустой маршрутизатор. Ни один сервис ещё не создан — всё ленивое.
Регистрирует фабрики-замыкания в контейнере. Фабрики на этом этапе НЕ вызываются.
Регистрирует определения маршрутов в маршрутизаторе. Callable-обработчики на этом этапе НЕ вызываются.
На FPM: строит PSR-7 ServerRequest из $_SERVER/$_FILES/$_POST. На персистентных воркерах: получает уже построенный запрос от рантайма.
Глобальный middleware разрешается из контейнера (лениво), маршрутизатор диспетчеризует, обработчик автосвязывается и вызывается.
На FPM: заголовки + тело записываются через header()/echo. На воркерах: передаётся обратно в цикл рантайма.
<?php
require 'vendor/autoload.php';
use Lift\App;
// ① Контейнер создан, маршрутизатор создан
$app = new App();
// ② Фабрики зарегистрированы — пока ничего не создано
$app->singleton(DB::class, fn() => new DB($_ENV['DSN']));
$app->singleton(Mailer::class, fn() => new Mailer($_ENV));
// ③ Маршруты зарегистрированы — обработчики ещё не вызваны
$app->use(LoggingMiddleware::class);
$app->group('/api', function ($g) {
$g->middleware(AuthMiddleware::class);
$g->get('/users', [UserController::class, 'index']);
$g->post('/users', [UserController::class, 'store']);
});
// ④ Получен первый запрос → выполняется конвейер
$app->run();
Модель памяти
Критическое различие между php-fpm и рантаймами с персистентным воркером — и что это значит для вашего кода.
- ▸ Полная стоимость загрузки на каждом запросе
- ▸ Никакого общего состояния между запросами
- ▸ Память освобождается автоматически — утечки невозможны
- ▸ Стоимость загрузки оплачивается один раз — не на каждый запрос
- ▸ Соединения с БД, кэши, скомпилированные маршруты остаются прогретыми
- ▸ Общие синглтоны сохраняются — не храните в них состояние уровня запроса
Написание кода, безопасного для воркеров
Синглтоны Lift переживают запросы. Держите их безсостоятельными или только дополняемыми.
class DB {
private \PDO $pdo;
public function __construct(string $dsn) {
// соединение разделяется — нормально
$this->pdo = new \PDO($dsn);
}
public function query(string $sql): array {
// безсостоятельный вызов — безопасно
return $this->pdo->query($sql)->fetchAll();
}
}
class UserContext {
// ⚠ переживает запросы в воркерах
public ?User $current = null;
public function setUser(User $u): void {
$this->current = $u; // утекает в следующий запрос!
}
}
// Решение: используйте атрибуты запроса:
// $request->withAttribute('user', $user)