Lift v1.3.0
Документація
На цій сторінці

Маршрутизація

Маршрутизатор зіставляє вхідний HTTP-запит (метод + шлях) з обробником (функцією/методом для виконання). Ця сторінка охоплює все, що може маршрутизатор — методи, параметри, іменовані маршрути, групи, middleware, маршрутизацію через атрибути та продакшен-кеш.

Ментальна модель: ви кажете маршрутизатору «коли GET потрапляє на /users/42, виклич цю функцію». Маршрутизатор перекладає це у швидку таблицю пошуку під час завантаження, а потім зіставляє кожен запит із нею.

П’ять базових методів

$app->get   ('/path', $handler);
$app->post  ('/path', $handler);
$app->put   ('/path', $handler);
$app->patch ('/path', $handler);
$app->delete('/path', $handler);

Менш поширені:

$app->any('/path', $handler);                        // GET POST PUT PATCH DELETE OPTIONS HEAD
$app->map(['GET', 'HEAD'], '/path', $handler);       // конкретний набір

Кожен виклик $app->get(...) повертає об’єкт Route, на якому можна будувати плавний ланцюжок (->name(), ->middleware()). Докладніше про це нижче.

Типи обробників

Обробник — це все, що викликається. Lift приймає п’ять різновидів і поводиться з усіма однаково:

// 1. Замикання
$app->get('/ping', fn() => 'pong');

// 2. Замикання з типізованими залежностями (автозв’язуються контейнером)
$app->get('/orders', function (Request $req, OrderService $svc) {
    return $svc->all();
});

// 3. [Class::class, 'method'] — клас розв’язується через DI-контейнер
$app->get('/users', [UserController::class, 'index']);

// 4. Invokable-клас (має __invoke)
$app->get('/healthcheck', HealthcheckAction::class);

// 5. Звичайне ім’я функції у вигляді рядка
$app->get('/old-school', 'my_handler_function');

Для варіантів 2 і 3 Lift дивиться на типи параметрів і автоматично бере екземпляри з контейнера. Вам ніколи не потрібно «реєструвати» контролери; вони автозв’язуються.

Що може повернути обробник

Повернене значення Що ви отримуєте назад
Об’єкт Response Передається без змін
array або object Response::json(...)
string Response::html(...)
null Response::noContent() (204)
будь-що інше (скаляр) Response::text((string) $v)

Тож fn() => ['ok' => true] і fn() => Response::json(['ok' => true]) еквівалентні. Використовуйте те, що читається краще.

Параметри маршруту

Усе, що всередині {...}, захоплюється й стає доступним через $req->param(...):

$app->get('/users/{id}', function (Request $req) {
    $id = $req->param('id');     // завжди рядок
    return ['id' => $id];
});

Отримати всі параметри разом:

$all = $req->params();           // ['id' => '42']

Regex-обмеження

За замовчуванням {param} відповідає [^/]+ — будь-якому символу, окрім /. Щоб вимагати конкретний шаблон, додайте :regex:

$app->get('/posts/{id:\d+}',             $handler);  // лише цифри
$app->get('/files/{name:[a-z0-9-]+}',    $handler);  // slug
$app->get('/cards/{code:[A-Z]{3}-\d{4}}', $handler); // "ABC-1234"

Шлях, який не відповідає regex, провалюється до наступного маршруту (або до 404, якщо нічого не підходить) — ваш обробник ніколи не викликається з некоректним значенням.

НЕПРАВИЛЬНО: іменовані групи PCRE у стилі (?P<name>...) — Lift використовує власний синтаксис плейсхолдерів, а не сирий PCRE. ПРАВИЛЬНО: {name} або {name:pattern}.

Необов’язкові сегменти

Lift не підтримує необов’язкові сегменти шляху всередині одного маршруту (стиль /users[/{id}]). Замість цього зареєструйте два маршрути:

$app->get('/users',       fn() => 'list');
$app->get('/users/{id}',  fn($req) => 'show ' . $req->param('id'));

Іменовані маршрути та генерація URL

Дайте маршруту ім’я через ->name(...), потім будуйте URL за іменем за допомогою $app->url(...):

$app->get('/users/{id}',        $h)->name('users.show');
$app->get('/articles/{slug}',   $h)->name('articles.show');

$app->url('users.show',    ['id'   => 42]);          // /users/42
$app->url('articles.show', ['slug' => 'hello']);     // /articles/hello

Угода про іменування — за вами: users.show, users:show, users-show усі працюють. Угода, що використовується в документації та генераторах, — resource.action (users.index, users.show, users.store, users.update, users.destroy).

Якщо ім’я не існує, $app->url(...) викидає RuntimeException.

Групи маршрутів

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

$app->group('/api/v1', function ($group) {
    $group->get   ('/users',          [UserController::class, 'index']);
    $group->get   ('/users/{id:\d+}', [UserController::class, 'show']);
    $group->post  ('/users',          [UserController::class, 'store']);
    $group->put   ('/users/{id:\d+}', [UserController::class, 'update']);
    $group->delete('/users/{id:\d+}', [UserController::class, 'destroy']);
});

Вкладені групи

$app->group('/api', function ($api) {
    $api->group('/v1', function ($v1) {
        $v1->get('/ping', fn() => ['pong' => 'v1']);
    });

    $api->group('/v2', function ($v2) {
        $v2->get('/ping', fn() => ['pong' => 'v2']);
    });
});

Middleware групи

Middleware, застосований до групи, виконується для кожного маршруту всередині неї (та успадковується у вкладені групи):

$app->group('/admin', function ($g) {
    $g->get('/users',    [AdminController::class, 'users']);
    $g->get('/settings', [AdminController::class, 'settings']);
})->middleware(AuthMiddleware::class, RequireAdminMiddleware::class);

Порядок має значення. Middleware, вказаний першим, виконується найзовнішнім — тобто бачить запит раніше за наступні middleware і бачить відповідь після них.

Помаршрутний middleware

Прикріпіть middleware до одного маршруту через ланцюжок ->middleware(...):

$app->post('/users', [UserController::class, 'store'])
    ->middleware(AuthMiddleware::class, RateLimitMiddleware::class);

Порядок виконання на запит:

Глобальний middleware (у порядку $app->use())
  → Middleware групи (від зовнішнього до внутрішнього)
    → Middleware маршруту (у порядку оголошення)
      → Обробник
    ← Middleware маршруту
  ← Middleware групи
← Глобальний middleware

Кожен шар може перервати ланцюжок, повернувши Response замість виклику $handler->handle($request).

404 і 405

Ситуація Викинуте виняток Відповідь за замовчуванням
URL не відповідає жодному маршруту Lift\Exception\NotFoundException 404 JSON
URL відповідає маршруту, але з невірним методом Lift\Exception\MethodNotAllowedException 405 JSON

Ви можете налаштувати відповідь через $app->onError(...):

use Lift\Exception\NotFoundException;
use Lift\Exception\MethodNotAllowedException;

$app->onError(function (\Throwable $e, Request $req) {
    if ($e instanceof NotFoundException) {
        return Response::html('<h1>404 — сторінку не знайдено</h1>', 404);
    }
    if ($e instanceof MethodNotAllowedException) {
        return Response::json(['error' => 'method not allowed'], 405);
    }
    return Response::json(['error' => 'server error'], 500);
});

Або під’єднайте конкретний тип винятку:

$app->onException(NotFoundException::class, fn() => Response::html('Not here.', 404));

Повний перелік винятків → Обробка помилок.

Маршрутизація через атрибути

Замість імперативного оголошення маршрутів можна прикріплювати атрибути #[Get], #[Post] тощо до методів контролерів. Маршрутизатор просканує класи, які ви попросите його завантажити.

use Lift\Attribute\Get;
use Lift\Attribute\Post;
use Lift\Attribute\Middleware;
use Lift\Attribute\Group;

#[Group('/api/v1/users')]
#[Middleware(AuthMiddleware::class)]
final class UserController
{
    public function __construct(private readonly UserRepository $users) {}

    #[Get('/')]
    public function index(): array
    {
        return $this->users->all();
    }

    #[Get('/{id:\d+}')]
    public function show(Request $req): Response { /* ... */ }

    #[Post('/')]
    #[Middleware(RateLimitMiddleware::class)]
    public function store(Request $req): Response { /* ... */ }
}

// у public/index.php
$app->loadControllers(UserController::class, OrderController::class, ...);

Повний довідник: Маршрутизація через атрибути.

Продакшен: кешування маршрутів

Коли у вас багато маршрутів (50+), сам крок реєстрації стає нетривіальним. Маршрутизатор може скомпілювати ваші маршрути у плоский PHP-файл, який OPcache завантажить миттєво:

$cache = __DIR__ . '/../storage/routes.cache.php';

$router = $app->router();   // скорочення для $app->container()->get(Router::class)

if (!$router->loadCache($cache)) {
    // Перший запит після деплою — реєструємо звичайним чином і пишемо кеш
    require __DIR__ . '/../routes/web.php';
    $router->writeCache($cache);
}

⚠️ Обробники-замикання мовчки пропускаються під час запису кешу. Використовуйте [Class::class, 'method'] або invokable-класи для будь-якого маршруту, який має кешуватися. Замикання все ще працюють, вони просто зводять нанівець кешування.

Очищайте кеш під час деплою (rm storage/routes.cache.php), і перший запит перебудує його.

Зручності App

$app надає два помічники, корисні, коли вам потрібен маршрутизатор або потрібно вручну надіслати відповідь:

// Прямий доступ до Router (корисно для кешу маршрутів, генерації URL поза запитом)
$app->router()->writeCache(storage_path('routes.cache.php'));
$app->router()->url('users.show', ['id' => 42]);

// Диспетчеризація + надсилання — звичайний випадок
$app->run();

// Диспетчеризація без надсилання (тестування, CLI-обгортки)
$response = $app->handle($request);
// … дослідити $response …
$app->send($response);   // надіслати, коли готово

Нотатки про продуктивність

  • Статичні маршрути — O(1). Маршрут без {param} живе у хеш-карті за ключем шлях + метод.
  • Динамічні маршрути — O(n). Лінійний перегляд із PCRE на кожен запит. Навіть 100 динамічних маршрутів розв’язуються за кілька мікросекунд.
  • Рефлексія кешується. Маршрутизатор рефлексує кожен обробник один раз на процес і повторно використовує метадані між запитами (величезне пришвидшення під OPcache + персистентними SAPI).
  • Карта іменованих маршрутів лінива. Будується один раз під час першого виклику url(), ніколи не перебудовується, доки не додано новий маршрут.

Часті підводні камені

Симптом Причина Виправлення
Маршрути реєструються, але повертають 404 Вебсервер не перенаправляє на index.php Перевірте конфігурацію Nginx/Apache в Встановленні.
{id} приходить як '42', а не 42 Параметри маршруту завжди рядки Приведіть тип: (int) $req->param('id').
Middleware не виконується Забули $app->use(...) або зчепили ->middleware(...) після ->name(...) (працює, але легко забути) Додайте його. Порядок не залежить від name().
Cannot resolve parameter $foo of type [App\X] Контейнер не знаходить прив’язки і клас не автозв’язуваний Або $app->bind(X::class, ...), або зробіть клас конкретним з автозв’язуваним конструктором.
$app->url('foo') викидає виняток Маршрут foo ніколи не реєструвався з ->name('foo') Зареєструйте його або виправте друкарську помилку.

Шпаргалка

// Методи
$app->get|post|put|patch|delete($path, $handler);
$app->any($path, $handler);
$app->map(['GET','POST'], $path, $handler);

// Параметри
'/users/{id}'              // без обмежень
'/users/{id:\d+}'          // обмежений regex

// Іменування + генерація URL
$app->get($p, $h)->name('foo');
$app->url('foo', ['id'=>1]);

// Групи
$app->group('/api', fn($g) => /* ... */)->middleware(M1::class);

// Помаршрутний middleware
$app->get($p, $h)->middleware(M1::class, M2::class);

// Маршрутизація через атрибути
$app->loadControllers(UserController::class);

// Продакшен-кеш
$router->loadCache($file) || ($router->writeCache($file));

Request →