Lift v1.3.0
Реальные сценарии использования

Сделано на Lift

Шесть продакшен-паттернов — от Telegram-ботов до ИИ-шлюзов — каждый показывает, почему Lift подходит для задачи.

Сценарий 1/6

Telegram-бот

Обработчик вебхуков + асинхронная отправка задач

Маршрутизация Очередь Middleware PSR-15

Почему Lift здесь подходит

  • Middleware PSR-15 проверяет X-Telegram-Bot-Api-Secret-Token от Telegram до запуска вашего кода
  • Тяжёлые задачи (отправка медиа, вызовы внешних API) отправляются в очередь Redis — вебхук мгновенно возвращает 200
  • То же приложение работает под RoadRunner для высоконагруженных ботов без перезапуска PHP на каждое обновление
telegram-bot.php
<?php
use Lift\App;
use Lift\Http\Request;
use Lift\Http\Response;
use Lift\Queue\RedisQueue;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class TelegramSecretMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $req, RequestHandlerInterface $next): ResponseInterface
    {
        if ($req->getHeaderLine('X-Telegram-Bot-Api-Secret-Token') !== $_ENV['TG_SECRET']) {
            return Response::json(['error' => 'Forbidden'], 403);
        }
        return $next->handle($req);
    }
}

$app = new App();
$app->setQueue(new RedisQueue($app->make(\Lift\Redis\RedisClient::class)));
$app->use(TelegramSecretMiddleware::class);

$app->post('/webhook', function (Request $req) use ($app) {
    $app->dispatch(new HandleTelegramUpdate($req->json()));
    return Response::noContent();
});

$app->run();
Сценарий 2/6

ИИ-шлюз

Потоковый прокси к LLM с аутентификацией и ограничением частоты

Аутентификация JWT Rate limit Поток SSE

Почему Lift здесь подходит

  • new SseResponse(fn(SseEmitter $emit) {...}) передаёт токены по мере поступления — без буферизации, без таймаута
  • RateLimitMiddleware принимает хранилище CacheInterface — замените ArrayCache на RedisCache в продакшене
  • JWT-middleware внедряет план/квоту вызывающего как атрибуты запроса — без обращения к БД на каждый запрос
ai-gateway.php
<?php
use Lift\Cache\RedisCache;
use Lift\Http\Request;
use Lift\Http\SseResponse;
use Lift\Http\SseEmitter;
use Lift\Http\SseEvent;
use Lift\Middleware\RateLimitMiddleware;
use Lift\Redis\RedisClient;

$app->group('/v1', function ($g) use ($app) {
    $g->middleware(new RateLimitMiddleware(
        store:         new RedisCache($app->make(RedisClient::class)),
        maxRequests:   60,
        windowSeconds: 60,
    ));
    $g->middleware(ApiKeyMiddleware::class);

    $g->post('/chat', function (Request $req) {
        $caller = $req->getAttribute('caller');

        return new SseResponse(function (SseEmitter $emit) use ($req, $caller) {
            $stream = openai_stream(
                model:  $caller->plan->model,
                prompt: $req->input('prompt'),
            );
            foreach ($stream as $token) {
                $emit(SseEvent::json(['token' => $token]));
            }
        });
    });
});
Сценарий 3/6

Бэкенд SaaS API

REST CRUD, аутентификация JWT, валидация, фоновые задачи

Валидация JWT БД Очередь

Почему Lift здесь подходит

  • Контроллеры — обычные классы, без базового класса, без магии. DI внедряет репозитории автоматически
  • $req->validate([rules]) объединяет тело + query + параметры маршрута и выбрасывает ValidationException при ошибке (авто-422)
  • Миграции + построитель запросов поставляются с фреймворком; добавьте предпочитаемый ORM при необходимости
saas-backend.php
<?php
use Lift\Http\Request;
use Lift\Http\Response;
use Lift\Queue\QueueInterface;

class ProjectController
{
    public function __construct(
        private readonly ProjectRepository $projects,
        private readonly QueueInterface $queue,
    ) {}

    public function store(Request $req): Response
    {
        // validate() merges body+query+route, throws 422 on failure
        $data = $req->validate([
            'name' => 'required|max:80',
            'plan' => 'required|in:free,pro,enterprise',
        ]);

        $project = $this->projects->create(
            $req->getAttribute('user')['sub'],
            $data,
        );

        $this->queue->push(new SendWelcomeEmail($project));

        return Response::json($project, 201);
    }
}

$app->group('/api/v1', function ($g) {
    $g->middleware(AuthMiddleware::class);
    $g->post('/projects', [ProjectController::class, 'store']);
    $g->get('/projects',  [ProjectController::class, 'index']);
});
Сценарий 4/6

Поток данных в реальном времени

Server-Sent Events + персистентный воркер RoadRunner

SSE RoadRunner Список Redis

Почему Lift здесь подходит

  • Производители добавляют события через lPush в список Redis; генератор SSE вычерпывает его через rPop
  • SseEvent::json($data) строит правильный сетевой формат; SseEmitter сбрасывает буфер после каждого события
  • RoadRunner держит воркер прогретым — соединение с Redis переиспользуется между потоками
realtime-feed.php
<?php
use Lift\App;
use Lift\Http\Request;
use Lift\Http\SseResponse;
use Lift\Http\SseEmitter;
use Lift\Http\SseEvent;
use Lift\Redis\RedisClient;
use Lift\Runtime\RoadRunnerWorker;

$app = new App();

// Producers elsewhere: $redis->lPush("feed:{$channel}", json_encode($event));
$app->get('/events/{channel}', function (Request $req) use ($app) {
    $key   = 'feed:' . $req->param('channel');
    $redis = $app->make(RedisClient::class);

    return new SseResponse(function (SseEmitter $emit) use ($redis, $key) {
        while (true) {
            $event = $redis->rPop($key);
            if ($event === false) {
                usleep(200_000); // nothing new — back off 200ms
                continue;
            }
            $emit(SseEvent::json(json_decode($event, true)));
        }
    });
});

(new RoadRunnerWorker($app))->serve();
Сценарий 5/6

Приёмник вебхуков

Быстрый приём, проверка HMAC, асинхронная обработка

Подписант Очередь Маршрутизация

Почему Lift здесь подходит

  • Signer::verify() использует hash_equals() — сравнение за константное время, защищённое от тайминг-атак
  • Вебхук возвращает 204 за микросекунды; фактическая обработка происходит в воркере очереди
  • Группы маршрутов позволяют разнести провайдеров по пространствам имён: /webhooks/stripe, /webhooks/github и т. д.
webhook-receiver.php
<?php
use Lift\Crypto\Signer;
use Lift\Http\Request;
use Lift\Http\Response;

$app->group('/webhooks', function ($g) use ($app) {

    $g->post('/stripe', function (Request $req) use ($app) {
        $signer = new Signer($_ENV['STRIPE_WEBHOOK_SECRET']);
        if (!$signer->verify((string) $req->getBody(), $req->getHeaderLine('Stripe-Signature'))) {
            return Response::json(['error' => 'Invalid signature'], 400);
        }

        $app->dispatch(new ProcessStripeEvent($req->json()));
        return Response::noContent();
    });

    $g->post('/github', function (Request $req) use ($app) {
        $signer = new Signer($_ENV['GITHUB_WEBHOOK_SECRET']);
        // GitHub sends "sha256=<hex>", strip the prefix
        $sig = substr($req->getHeaderLine('X-Hub-Signature-256'), 7);
        if (!$signer->verify((string) $req->getBody(), $sig)) {
            return Response::json(['error' => 'Invalid signature'], 400);
        }

        $app->dispatch(new ProcessGithubEvent($req->json()));
        return Response::noContent();
    });
});
Сценарий 6/6

Сервер аутентификации

Выпуск JWT, refresh-токены, вход с ограничением частоты

JWT Шифровальщик Rate limit

Почему Lift здесь подходит

  • Jwt::encode($payload) — exp это стандартный claim в массиве payload, отдельного параметра ttl нет
  • Encrypter (AES-256-GCM) шифрует полезную нагрузку refresh-токена — защищён от подделки без обращения к БД
  • RateLimitMiddleware на маршруте блокирует перебор до запуска обработчика
auth-server.php
<?php
use Lift\Cache\RedisCache;
use Lift\Crypto\Encrypter;
use Lift\Http\Request;
use Lift\Http\Response;
use Lift\Jwt\Jwt;
use Lift\Middleware\RateLimitMiddleware;
use Lift\Redis\RedisClient;

// Encrypter needs a 32-byte key, Jwt needs a secret — register both
$app->singleton(Jwt::class, fn() => new Jwt(secret: $_ENV['JWT_SECRET']));
$app->singleton(Encrypter::class, fn() => new Encrypter(base64_decode($_ENV['APP_KEY'])));

$app->post('/auth/login', function (Request $req) use ($app) {
    $data = $req->validate([
        'email'    => 'required|email',
        'password' => 'required',
    ]);

    $user = UserRepository::findByEmail($data['email']);
    if (!$user || !password_verify($data['password'], $user->password_hash)) {
        return Response::json(['error' => 'Invalid credentials'], 401);
    }

    $jwt       = $app->make(Jwt::class);
    $encrypter = $app->make(Encrypter::class);

    // exp goes inside the payload — no separate ttl param
    $accessToken = $jwt->encode([
        'sub'  => $user->id,
        'role' => $user->role,
        'exp'  => time() + 900,
    ]);

    $refreshToken = $encrypter->encrypt(json_encode([
        'uid' => $user->id,
        'exp' => time() + 2_592_000,
    ]));

    return Response::json([
        'access_token'  => $accessToken,
        'refresh_token' => $refreshToken,
        'expires_in'    => 900,
    ]);
})->middleware(new RateLimitMiddleware(
    store:         new RedisCache($app->make(RedisClient::class)),
    maxRequests:   5,
    windowSeconds: 60,
));

Начните строить

Все паттерны выше работают «из коробки». Никаких дополнительных пакетов для JWT, шифрования, SSE, очередей или ограничения частоты.