Архітектура
Як завантажується застосунок 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)