Lift v1.3.0
Внутрішня будова

Архітектура

Як завантажується застосунок Lift, як запит проходить крізь нього і як контейнер розв’язує залежності. Жодних сюрпризів — усе явно.

Життєвий цикл запиту

Від моменту, коли PHP отримує HTTP-запит, до моменту, коли надіслано відповідь.

HTTP-запит
PHP отримує сирий запит (FPM, RoadRunner, Swoole, FrankenPHP)
PSR-7 ServerRequest
Lift загортає суперглобалі PHP у незмінний об’єкт PSR-7
Стек глобального middleware
Кожен зареєстрований глобальний middleware виконується в порядку реєстрації
Маршрутизатор
Статичні маршрути → хеш-пошук за O(1). Динамічні → скомпільований regex
Middleware маршруту
Помаршрутний / погруповий middleware виконується всередині глобального стека
Розв’язання обробника
DI-контейнер автозв’язує залежності конструктора; обробник викликається
Авто-приведення відповіді
array → JSON 200, string → HTML 200, null → 204, Response → як є
PSR-7 Response
Незмінний об’єкт відповіді передається назад угору ланцюжком middleware
Надсилання
Заголовки + тіло записуються клієнту (або повертаються рантайму)

Конвеєр middleware

Цибулинна модель PSR-15 — кожен шар загортає наступний. Найвнутрішніший шар — обробник маршруту.

// Запит тече всередину ↓, відповідь тече назовні ↑
┌─ GlobalMiddleware A ────────────────────┐
│  ┌─ GlobalMiddleware B ───────────────┐ │
│  │  ┌─ GroupMiddleware ─────────────┐ │ │
│  │  │  ┌─ RouteMiddleware ────────┐ │ │ │
│  │  │  │  ┌─ Handler ──────────┐ │ │ │ │
│  │  │  │  │   return $response  │ │ │ │ │
│  │  │  │  └────────────────────── ┘ │ │ │ │
│  │  │  └────────────────────────────┘ │ │ │
│  │  └──────────────────────────────── ┘ │ │
│  └────────────────────────────────────── ┘ │
└────────────────────────────────────────────┘
реєстрація middleware
// 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);
});
Контракт PSR-15: Кожен middleware реалізує MiddlewareInterface::process(ServerRequestInterface, RequestHandlerInterface). Він може перервати ланцюжок, повернувши відповідь напряму — внутрішні шари тоді не викликаються. Middleware автентифікації та обмеження частоти використовують це, щоб відхиляти неавтентифіковані запити до запуску будь-якого обробника.

Будова маршрутизатора

Дворівнева диспетчеризація: хеш-карта за O(1) для статичних маршрутів і перегляд скомпільованих regex по кожному маршруту для динамічних.

Рівень 1 — статичні маршрути

GET /, POST /auth/login — маршрути без параметрів зберігаються у вкладеному масиві за ключем [method][path]. Розв’язання це одне читання масиву: O(1), без сканування рядків.

$staticRoutes['GET']['/'] = $handler;
// пошук: isset($staticRoutes[$method][$path])
Рівень 2 — динамічні маршрути

Маршрути з параметрами (/users/{id:\d+}) кожен компілює свій regex один раз — кешується на об’єкті маршруту, не перебудовується на кожен запит. Диспетчеризація переглядає динамічні маршрути по порядку, перевіряючи кожен скомпільований regex, доки один не збіжиться.

// regex на маршрут, скомпільований і закешований один раз
~^/users/(?<id>\d+)$~
Обмеження маршруту: Вбудовані regex-обмеження ({id:\d+}) запікаються у скомпільований regex — маршрут збігається лише коли обмеження проходить, тому додатковий крок валідації у вашому обробнику не потрібен.

DI-контейнер

Контейнер PSR-11 з автозв’язуванням конструктора. Три режими реєстрації, один алгоритм розв’язання.

bind()

Фабрика викликається щоразу під час виклику make(). Використовуйте для безстанових сервісів.

$app->bind(Mailer::class, fn() => new Mailer($_ENV));
singleton()

Фабрика викликається один раз; екземпляр кешується і повторно використовується під час кожного наступного make().

$app->singleton(DB::class, fn() => new DB($_ENV['DSN']));
instance()

Зберігає вже побудований об’єкт напряму. Корисно, коли ви конструюєте значення до його реєстрації.

$app->instance(Config::class, $config);

Алгоритм автозв’язування

Коли make(ClassName::class) викликається без зареєстрованої прив’язки:

1
Перевірити реєстр прив’язок
Якщо існує bind/singleton/instance → використати його. Без рефлексії.
2
Перевірити кеш рефлексії
Параметри конструктора кешуються після першого розв’язання. Без повторного читання на повторних викликах.
3
Прочитати параметри конструктора
ReflectionClass оглядає підказки типів. Примітиви без значень за замовчуванням → ContainerException.
4
Рекурсивно розв’язати залежності
Кожен типізований параметр розв’язується через make() — усе дерево зв’язується автоматично.
5
Створити екземпляр
new ClassName(...$resolvedArgs) — стандартне створення PHP, без проксі-об’єктів.
6
Закешувати результат рефлексії
Список параметрів зберігається; наступний make() повністю пропускає кроки 2–4.

Завантаження

Що відбувається між new App() і обслуговуванням першого запиту.

1 new App()

Створює порожній контейнер PSR-11 і порожній маршрутизатор. Жоден сервіс ще не створено — усе ліниве.

2 bind / singleton / instance

Реєструє фабрики-замикання у контейнері. Фабрики на цьому етапі НЕ викликаються.

3 get / post / group

Реєструє визначення маршрутів у маршрутизаторі. Callable-обробники на цьому етапі НЕ викликаються.

4 run() / RoadRunner serve()

На FPM: будує PSR-7 ServerRequest із $_SERVER/$_FILES/$_POST. На персистентних воркерах: отримує вже побудований запит від рантайму.

5 Виконується конвеєр middleware

Глобальний middleware розв’язується з контейнера (ліниво), маршрутизатор диспетчеризує, обробник автозв’язується і викликається.

6 Відповідь надіслано

На FPM: заголовки + тіло записуються через header()/echo. На воркерах: передається назад у цикл рантайму.

bootstrap.php (типовий)
<?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 і рантаймами з персистентним воркером — і що це означає для вашого коду.

php-fpm (традиційний) на запит
// Запит 1
завантаження → new App()new DB() → обробка → 💀 знищити все
// Запит 2
завантаження → new App()new DB() → обробка → 💀 знищити все
// Запит N — та сама історія
  • Повна вартість завантаження на кожному запиті
  • Жодного спільного стану між запитами
  • Пам’ять звільняється автоматично — витоки неможливі
RoadRunner / Swoole / FrankenPHP персистентний
// Завантаження один раз
new App()new DB()✓ залишаються в пам’яті
// Запит 1..N — повторно використовуємо прогрітий стан
прийом → обробка → надсилання → цикл
прийом → обробка → надсилання → цикл
прийом → обробка → надсилання → цикл
  • Вартість завантаження оплачується один раз — не на кожен запит
  • З’єднання з БД, кеші, скомпільовані маршрути залишаються прогрітими
  • Спільні синглтони зберігаються — не зберігайте в них стан рівня запиту

Написання коду, безпечного для воркерів

Синглтони 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)