Быстрый старт
К концу этой страницы вы построите небольшой JSON-API с несколькими маршрутами, валидацией параметров, внедрением зависимостей и классом-контроллером — используя только то, что входит в поставку Lift.
Начнём с буквально однострочника и постепенно вырастим его во что-то, узнаваемое по реальному сервису.
Этап 0 — Hello, World
Если вы прошли Установку, public/index.php уже выглядит так:
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Lift\App;
use Lift\Http\Response;
$app = new App();
$app->get('/', fn() => Response::json(['message' => 'Hello, World!']));
$app->run();
Запустите:
php -S 127.0.0.1:8000 -t public
curl http://127.0.0.1:8000/
# {"message":"Hello, World!"}
Три вещи, на которые стоит обратить внимание:
new App()— никакой фабрики, никакого билдера. Приложение — это просто объект.$app->get('/', $handler)—$handlerможет быть чем угодно вызываемым: замыканием,[Class::class, 'method']или именем invokable-класса.Response::json([...])— один из нескольких фабричных сокращений. Все они описаны в Response.
Если ваш обработчик возвращает
array, Lift автоматически оборачивает его вResponse::json(...). Так чтоfn() => ['ok' => true]тоже работает — см. автоматическое преобразование ответа. В остальной части руководства мы будем явными и используемResponse::json(...).
Этап 1 — Несколько маршрутов
$app->get('/', fn() => Response::json(['message' => 'Hello, World!']));
$app->get('/health', fn() => Response::json(['ok' => true]));
$app->get('/version', fn() => Response::json(['version' => '1.0.0']));
// POST / PUT / PATCH / DELETE работают точно так же
$app->post('/echo', function (\Lift\Http\Request $req) {
return Response::json(['you_sent' => $req->json()]);
});
Быстрая проверка:
curl -X POST -H 'Content-Type: application/json' \
-d '{"foo":"bar"}' \
http://127.0.0.1:8000/echo
# {"you_sent":{"foo":"bar"}}
Обратите внимание на параметр замыкания \Lift\Http\Request $req. Lift автоматически внедряет текущий запрос в любой обработчик, который его запрашивает. Передавать его вручную не нужно.
Этап 2 — Параметры маршрута
Всё, что внутри {...}, становится параметром, который можно прочитать через $req->param(...):
$app->get('/users/{id}', function (\Lift\Http\Request $req) {
return Response::json([
'id' => $req->param('id'),
]);
});
curl http://127.0.0.1:8000/users/42
# {"id":"42"}
Заметьте, что id приходит как строка — именно это даёт вам HTTP. Приводите тип сами:
$id = (int) $req->param('id');
Параметр также можно ограничить regex-шаблоном. Двоеточие отделяет имя от шаблона:
$app->get('/posts/{id:\d+}', $handler); // только цифры
$app->get('/articles/{slug:[a-z0-9-]+}', $handler); // строчные буквы + дефисы
Если URL не соответствует шаблону, Lift возвращает 404 — обработчик не вызывается.
Этап 3 — Крошечный REST API в памяти
Построим что-то близкое к настоящему CRUD-эндпоинту, используя в качестве «базы данных» обычный массив PHP:
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Lift\App;
use Lift\Http\Request;
use Lift\Http\Response;
$app = new App();
/** @var array<int, array{id:int, name:string}> $users */
$users = [
1 => ['id' => 1, 'name' => 'Alice'],
2 => ['id' => 2, 'name' => 'Bob'],
];
$nextId = 3;
// Список
$app->get('/users', fn() => Response::json(array_values($users)));
// Просмотр
$app->get('/users/{id:\d+}', function (Request $req) use (&$users) {
$id = (int) $req->param('id');
if (!isset($users[$id])) {
return Response::json(['error' => 'User not found'], 404);
}
return Response::json($users[$id]);
});
// Создание
$app->post('/users', function (Request $req) use (&$users, &$nextId) {
$name = $req->json()['name'] ?? null;
if (!is_string($name) || $name === '') {
return Response::json(['error' => 'name is required'], 422);
}
$id = $nextId++;
$users[$id] = ['id' => $id, 'name' => $name];
return Response::json($users[$id], 201);
});
// Обновление
$app->put('/users/{id:\d+}', function (Request $req) use (&$users) {
$id = (int) $req->param('id');
if (!isset($users[$id])) {
return Response::json(['error' => 'User not found'], 404);
}
$users[$id]['name'] = $req->json()['name'] ?? $users[$id]['name'];
return Response::json($users[$id]);
});
// Удаление
$app->delete('/users/{id:\d+}', function (Request $req) use (&$users) {
unset($users[(int) $req->param('id')]);
return Response::noContent(); // 204
});
$app->run();
Попробуйте:
curl http://127.0.0.1:8000/users
curl http://127.0.0.1:8000/users/1
curl -X POST -H 'Content-Type: application/json' -d '{"name":"Carol"}' http://127.0.0.1:8000/users
curl -X PUT -H 'Content-Type: application/json' -d '{"name":"Bobby"}' http://127.0.0.1:8000/users/2
curl -X DELETE http://127.0.0.1:8000/users/1
Что вы могли упустить:
$req->json()возвращает декодированное тело JSON в виде ассоциативного массива. Всегда.Response::json($data, 201)позволяет задать собственный код состояния.Response::noContent()— это сокращение для HTTP 204.Response::json(['error' => ...], 422)— общепринятая форма для ошибок валидации.
Этап 4 — Валидация, простой способ
Писать вручную проверки if (!$name) быстро надоедает. В Lift есть валидатор. Самый короткий способ его применить — $req->validate([...]):
use Lift\Validation\ValidationException;
$app->post('/users', function (Request $req) use (&$users, &$nextId) {
try {
$data = $req->validate([
'name' => 'required|string|min:2|max:255',
'email' => 'required|email',
]);
} catch (ValidationException $e) {
return Response::json(['errors' => $e->errors()], 422);
}
$id = $nextId++;
$users[$id] = ['id' => $id, ...$data];
return Response::json($users[$id], 201);
});
try/catch на самом деле не обязателен: если ValidationException покидает обработчик, обработчик ошибок Lift по умолчанию преобразует его в 422 с той же формой errors. Полный список правил находится в Валидации.
Этап 5 — Контроллеры и внедрение зависимостей
Замыкания в index.php годятся для 10 маршрутов. Дальше вам понадобятся классы. Контейнер Lift создаст их и внедрит их зависимости автоматически.
my-app/
├── public/index.php
└── src/
├── UserRepository.php
└── UserController.php
src/UserRepository.php:
<?php
namespace App;
final class UserRepository
{
/** @var array<int, array{id:int, name:string}> */
private array $users = [
1 => ['id' => 1, 'name' => 'Alice'],
2 => ['id' => 2, 'name' => 'Bob'],
];
private int $nextId = 3;
public function all(): array { return array_values($this->users); }
public function find(int $id): ?array { return $this->users[$id] ?? null; }
public function create(array $data): array
{
$id = $this->nextId++;
return $this->users[$id] = ['id' => $id, ...$data];
}
}
src/UserController.php:
<?php
namespace App;
use Lift\Http\Request;
use Lift\Http\Response;
final class UserController
{
// 👇 Lift автоматически связывает это через контейнер — вы никогда не создаёте UserController сами.
public function __construct(private readonly UserRepository $users) {}
public function index(): array
{
return $this->users->all();
}
public function show(Request $req): Response
{
$user = $this->users->find((int) $req->param('id'));
return $user
? Response::json($user)
: Response::json(['error' => 'Not found'], 404);
}
public function store(Request $req): Response
{
$data = $req->validate([
'name' => 'required|string|min:2',
'email' => 'required|email',
]);
return Response::json($this->users->create($data), 201);
}
}
Сообщите Composer о пространстве имён — добавьте это в composer.json и выполните composer dump-autoload:
"autoload": {
"psr-4": { "App\\": "src/" }
}
Наконец, подключите маршруты в public/index.php:
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Lift\App;
use App\UserController;
use App\UserRepository;
$app = new App();
// Кэшируем репозиторий на время жизни запроса
$app->singleton(UserRepository::class);
$app->get ('/users', [UserController::class, 'index']);
$app->get ('/users/{id:\d+}', [UserController::class, 'show']);
$app->post ('/users', [UserController::class, 'store']);
$app->run();
Вот и всё. Обратите внимание, что:
- Вы никогда явно не создаёте
UserControllerилиUserRepository— это делает контейнер через автосвязывание. $app->singleton(UserRepository::class)говорит контейнеру «создай один и переиспользуй». Без этой строки вы получали бы новый репозиторий на каждый вызов (и теряли бы данные в памяти).[UserController::class, 'index']— это стандартный для PHP callable вида[$class, $method]. Lift разрешает класс через контейнер, а затем вызывает метод.
Этап 6 — Группировка маршрутов
Большинство API версионируют свои эндпоинты под /api/v1/.... Группы берут префикс на себя:
$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']);
});
Группы также принимают middleware, префиксы для именованных маршрутов и могут быть вложенными. См. Маршрутизация.
Этап 7 — Добавление middleware
Допустим, вы хотите, чтобы каждый ответ нёс заголовок X-Request-Id. Middleware — правильное для этого место.
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class RequestIdMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $req, RequestHandlerInterface $next): ResponseInterface
{
$id = $req->getHeaderLine('X-Request-Id') ?: bin2hex(random_bytes(8));
return $next->handle($req->withAttribute('request_id', $id))
->withHeader('X-Request-Id', $id);
}
}
$app->use(RequestIdMiddleware::class); // глобально — выполняется на каждом запросе
У маршрутов также может быть помаршрутный или погрупповой middleware. См. Middleware.
Этап 8 — Подключение настоящего хранилища
Замените массив в памяти на SQLite (или MySQL/Postgres) менее чем в 20 строк:
use Lift\Database\Connection;
$app->singleton(Connection::class, fn() => Connection::fromConfig([
'driver' => 'sqlite',
'database' => __DIR__ . '/../database.sqlite',
]));
// В вашем репозитории:
public function __construct(private readonly Connection $db) {}
public function all(): array
{
return $this->db->table('users')->orderBy('id')->get();
}
Полное руководство, включая миграции: База данных.
Куда двигаться дальше
Теперь вы знаете достаточно, чтобы построить настоящий CRUD-сервис. Естественные следующие шаги:
| Если вы хотите… | Читайте |
|---|---|
| Понять каждую возможность маршрутизации | Маршрутизация |
| Читать ввод из форм, JSON, файлов | Request |
| Отправлять HTML, ставить cookie, делать редирект | Response |
| Подключать сервисы без глобалей | DI-контейнер |
| Добавить аутентификацию, CORS, ограничение частоты | Middleware, Безопасность |
| Правильно валидировать ввод | Валидация |
| Работать с базой данных | База данных |
| Обрабатывать фоновые задачи | Очереди |
| Писать тесты | Тестирование |
| Развернуть в продакшене | Установка §6 |