Lift v1.3.0

Приклади

Невеликі готові до копіювання рецепти, що показують, як Lift розв’язує реальні задачі.

API «Hello, World»

Мінімально можливий застосунок Lift — JSON на вході, JSON на виході.

<?php
require 'vendor/autoload.php';

use Lift\App;
use Lift\Http\Response;

$app = new App();

$app->get('/', fn() => Response::json(['hello' => 'world']));

$app->run();

REST CRUD із контролером

Типізований контролер, що автоматично розв’язується через контейнер.

<?php
use Lift\App;
use Lift\Http\Request;
use Lift\Http\Response;

class UserController
{
    public function __construct(private readonly UserRepository $users) {}

    public function index(): array
    {
        return $this->users->all();          // -> авто JSON
    }

    public function show(Request $r): Response
    {
        $user = $this->users->find((int) $r->param('id'));
        return $user
            ? Response::json($user)
            : Response::json(['error' => 'Not found'], 404);
    }

    public function store(Request $r): Response
    {
        $id = $this->users->create($r->json());
        return Response::json(['id' => $id], 201);
    }
}

$app = new App();

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

$app->run();

Auth-middleware + JWT

Перевірка Bearer-токена в middleware, прикріплення користувача до запиту.

<?php
use Lift\Http\Response;
use Lift\Jwt\Jwt;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(private readonly Jwt $jwt) {}

    public function process(ServerRequestInterface $r, RequestHandlerInterface $h): ResponseInterface
    {
        [, $token] = explode(' ', $r->getHeaderLine('Authorization') . ' ', 2);
        try {
            $claims = $this->jwt->decode($token);
        } catch (\Throwable) {
            return Response::json(['error' => 'Unauthorized'], 401);
        }

        return $h->handle($r->withAttribute('user', $claims));
    }
}

$app->singleton(Jwt::class, fn() => new Jwt(secret: $_ENV['APP_KEY']));

$app->get('/me', fn(Request $r) => $r->getAttribute('user'))
    ->middleware(AuthMiddleware::class);

Валідація

Плавні правила — за помилки автоматично повертається 422 з картою помилок.

<?php
$app->post('/signup', function (Request $r) {
    // validate() об’єднує тіло + query + параметри маршруту, викидає ValidationException за помилки
    $data = $r->validate([
        'email'    => 'required|email',
        'password' => 'required|min_length:8',
        'age'      => 'integer|min:13',
    ]);

    return Response::json(['user' => createUser($data)], 201);
});

Фонова задача через чергу

Поміщаємо задачу з обробника; воркер її підхоплює.

<?php
use Lift\Queue\AbstractJob;
use Lift\Queue\RedisQueue;
use Lift\Redis\RedisClient;

// AbstractJob надає failed()/getQueue()/getDelay()/getTries();
// ви реалізуєте лише handle().
class SendWelcomeEmail extends AbstractJob
{
    public function __construct(private string $email) {}

    public function handle(): void
    {
        // ... надіслати лист
    }
}

$app->setQueue(new RedisQueue($app->make(RedisClient::class)));

$app->post('/signup', function (Request $r) use ($app) {
    $email = $r->input('email');
    $app->dispatch(new SendWelcomeEmail($email));
    return Response::noContent();
});

Ендпоінт JSON-RPC 2.0

Прив’яжіть JsonRpcServer до одного маршруту — повна відповідність специфікації.

<?php
use Lift\JsonRpc\JsonRpcServer;

$rpc = new JsonRpcServer();

// Параметри впроваджуються за іменем (об’єкт params) або за позицією (масив params)
$rpc->register('math.add', fn(int $a, int $b): int => $a + $b);
$rpc->register('math.mul', fn(int $a, int $b): int => $a * $b);

$app->rpc('/rpc', $rpc);

Server-Sent Events

Потокове надсилання живих оновлень клієнту.

<?php
use Lift\Http\SseEmitter;
use Lift\Http\SseEvent;
use Lift\Http\SseResponse;

$app->get('/stream/clock', function () {
    return new SseResponse(function (SseEmitter $emit) {
        while (true) {
            $emit(SseEvent::json(['ts' => date('c')]));
            sleep(1);
        }
    });
});

Високопродуктивні рантайми

Готові до під’єднання воркери для RoadRunner, Swoole і FrankenPHP — тримають PHP у пам’яті між запитами без жодної зміни коду застосунку.

RoadRunner

spiral/roadrunner-http nyholm/psr7

PHP залишається в пам’яті між запитами — без витрат на bootstrap на кожен запит. Lift автоматично визначає встановлену фабрику PSR-17 (Nyholm → Guzzle → Laminas).

<?php
// public/worker.php
require 'vendor/autoload.php';

use Lift\App;
use Lift\Http\Response;
use Lift\Runtime\RoadRunnerWorker;

$app = new App();
$app->get('/', fn() => Response::json(['status' => 'ok']));
$app->get('/users/{id:\d+}', fn(\Lift\Http\Request $r) => ['id' => $r->param('id')]);

(new RoadRunnerWorker($app))->serve();

.rr.yaml → server: {command: "php public/worker.php"}

Swoole / OpenSwoole

ext-swoole

Lift перетворює \Swoole\Http\Request → Lift Request і записує назад Lift Response — ваші обробники маршрутів залишаються ідентичними.

<?php
require 'vendor/autoload.php';

use Lift\App;
use Lift\Http\Response;
use Lift\Runtime\SwooleServer;

$app = new App();
$app->get('/', fn() => Response::json(['status' => 'ok']));
$app->get('/users/{id:\d+}', fn(\Lift\Http\Request $r) => ['id' => $r->param('id')]);

(new SwooleServer($app, [
    'host'       => '0.0.0.0',
    'port'       => 9501,
    'worker_num' => swoole_cpu_num(),
]))->start();

FrankenPHP

режим воркера

Той самий public/index.php працює і в режимі воркера FrankenPHP, і в класичному php-fpm — окрема точка входу не потрібна.

<?php
require 'vendor/autoload.php';

use Lift\App;
use Lift\Http\Response;
use Lift\Runtime\FrankenPhpWorker;

$app = new App();
$app->get('/', fn() => Response::json(['status' => 'ok']));

// Режим воркера FrankenPHP: PHP крутиться в циклі на frankenphp_handle_request().
// Відкочується до одного циклу запиту для php-fpm / php -S.
if (function_exists('frankenphp_handle_request')) {
    (new FrankenPhpWorker($app))->serve();
} else {
    $app->run();
}