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, черг чи обмеження частоти.