Версия 1 REST API на PHP: от проектирования до реализации
Основной подход: использование микрофреймворка Slim для версии 1 API
Для быстрого и эффективного создания REST API v1 рекомендуется применять легковесный фреймворк Slim (версии 4). Он предоставляет удобную маршрутизацию, обработку HTTP-запросов и ответов, а также поддержку middleware.
Как настроить маршрутизацию для версии 1 API?
Создайте проект с помощью Composer: composer create-project slim/slim-skeleton api-v1. В файле public/index.php определите маршруты с префиксом /v1:
<?php
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Группа маршрутов для v1
$app->group('/v1', function (\Slim\Routing\RouteCollectorProxy $group) {
$group->get('/users', 'App\Controllers\UserController:index');
$group->get('/users/{id}', 'App\Controllers\UserController:show');
$group->post('/users', 'App\Controllers\UserController:create');
});
$app->run();Api v1 php (api v1 на php)
В контроллере возвращайте JSON-ответ через объект Response:
<?php
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class UserController
{
public function index(Request $request, Response $response): Response
{
$data = [
['id' => 1, 'name' => 'Иван'],
['id' => 2, 'name' => 'Мария']
];
$payload = json_encode($data, JSON_UNESCAPED_UNICODE);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
}
public function show(Request $request, Response $response, array $args): Response
{
$id = $args['id'];
// ... получение из БД
$user = ['id' => (int)$id, 'name' => 'Анна'];
$payload = json_encode($user, JSON_UNESCAPED_UNICODE);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
}
}
Типичные ошибки:
- Отсутствие заголовка Content-Type: клиент может интерпретировать ответ как HTML. Всегда добавляйте
->withHeader('Content-Type', 'application/json'). - Проблемы с CORS: если API вызывается с другого домена, добавьте middleware для CORS. Slim предоставляет готовый
tuupola/cors-middleware. - Неправильное кодирование UTF-8: используйте флаг
JSON_UNESCAPED_UNICODEпри кодировании.
Альтернативные реализации
Вариант 1. Чистый PHP без фреймворка
Когда стоит задача полностью контролировать код или проект минимален, можно написать API v1 на чистом PHP. Вопрос: как обработать URL для версии 1 без фреймворка?
<?php
// index.php
$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
// Проверяем префикс v1
if (preg_match('#^/v1/users(?:/(\d+))?#', $uri, $matches)) {
header('Content-Type: application/json; charset=utf-8');
$id = $matches[1] ?? null;
if ($method === 'GET') {
if ($id) {
echo json_encode(['id' => (int)$id, 'name' => 'Пользователь'], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode([['id' => 1, 'name' => 'Иван']], JSON_UNESCAPED_UNICODE);
}
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
// валидация и сохранение
echo json_encode(['status' => 'created', 'id' => 3], JSON_UNESCAPED_UNICODE);
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
} else {
http_response_code(404);
echo json_encode(['error' => 'Not found']);
}
Как организовать структуру папок для такого подхода?
Разделите логику на отдельные файлы: routes.php, controllers/UserController.php. Подключайте их в index.php. Проблема: маршрутизация быстро становится громоздкой, сложно поддерживать версионирование.
Ошибки: легко пропустить проверку HTTP-метода, некорректно обработать тело запроса (php://input читается один раз). Решение: всегда проверять метод и использовать file_get_contents('php://input') единоразово.
Вариант 2. Использование Laravel для API v1
Laravel предоставляет мощный инструментарий для API: ресурсные контроллеры, валидацию, аутентификацию через Passport/Sanctum. Вопрос: как определить маршруты для версии 1 в Laravel?
// routes/api.php
Route::prefix('v1')->group(function () {
Route::apiResource('users', 'Api\V1\UserController');
// Автоматически создаёт маршруты: GET /v1/users, POST /v1/users, GET /v1/users/{user} и т.д.
});
Контроллер обычно наследует App\Http\Controllers\Controller и использует json() для ответа:
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function index()
{
return response()->json(User::all(), 200, [], JSON_UNESCAPED_UNICODE);
}
public function show($id)
{
$user = User::findOrFail($id);
return response()->json($user, 200, [], JSON_UNESCAPED_UNICODE);
}
}
Как добавить аутентификацию через токен?
Laravel Sanctum позволяет легко защитить маршруты. Установите пакет, затем в маршрутах:
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::apiResource('users', 'Api\V1\UserController');
});
Ошибка: если не настроить .env для базы данных, Laravel выдаст исключение. Решение: выполните миграции и корректно укажите соединение.
Также следует обрабатывать исключения в App\Exceptions\Handler для возврата JSON вместо HTML.
Расширенные примеры и нестандартные ситуации
Дополнительные сценарии для API v1, которые помогут избежать проблем в продакшене.
Версионирование через заголовок Accept
Как реализовать версионирование API без изменения URL?
Вместо префикса /v1 можно использовать заголовок Accept с указанием версии. Пример с Slim:
<?php
$app->add(function (Request $request, RequestHandler $handler) {
$accept = $request->getHeaderLine('Accept');
if (strpos($accept, 'application/vnd.api.v1+json') !== false) {
$request = $request->withAttribute('api_version', 'v1');
}
return $handler->handle($request);
});
$app->get('/users', function (Request $request, Response $response) {
$version = $request->getAttribute('api_version', 'v1');
$data = [];
if ($version === 'v1') {
$data = ['id' => 1, 'name' => 'Иван'];
}
$payload = json_encode($data, JSON_UNESCAPED_UNICODE);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/vnd.api.v1+json');
});
Результат при запросе с заголовком Accept: application/vnd.api.v1+json:
{"id":1,"name":"Иван"}
Пагинация и фильтрация
Как добавить постраничный вывод списка пользователей API v1?
Ресурс /v1/users?page=2&limit=10. Пример на Slim с ручной обработкой:
<?php
$app->get('/v1/users', function (Request $request, Response $response) {
$params = $request->getQueryParams();
$page = max(1, (int)($params['page'] ?? 1));
$limit = min(100, max(1, (int)($params['limit'] ?? 10)));
$offset = ($page - 1) * $limit;
// Предположим, есть функция getUsers() возвращающая массив
$users = getUsers($offset, $limit);
$total = getTotalUsers();
$result = [
'data' => $users,
'meta' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit),
]
];
$payload = json_encode($result, JSON_UNESCAPED_UNICODE);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
});
Запрос GET /v1/users?page=1&limit=2 вернёт:
{"data":[{"id":1,"name":"Иван"},{"id":2,"name":"Мария"}],"meta":{"page":1,"limit":2,"total":10,"pages":5}}
Кастомные исключения и обработка ошибок
Как вернуть стандартизированный JSON при ошибке (400, 404, 500)?
Глобальная обработка исключений в Slim через интерфейс ErrorHandlerInterface:
<?php
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpMethodNotAllowedException;
$app->addErrorMiddleware(true, true, true)
->setDefaultErrorHandler(function (
ServerRequestInterface $request,
Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app) {
$statusCode = 500;
if ($exception instanceof HttpNotFoundException) {
$statusCode = 404;
} elseif ($exception instanceof HttpMethodNotAllowedException) {
$statusCode = 405;
}
$response = $app->getResponseFactory()->createResponse($statusCode);
$payload = json_encode([
'error' => [
'code' => $statusCode,
'message' => $exception->getMessage()
]
], JSON_UNESCAPED_UNICODE);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
});
Результат при запросе несуществующего маршрута:
{"error":{"code":404,"message":"Not found."}}
Валидация входящих данных
Как проверить поля при создании пользователя (POST /v1/users)?
Используйте стороннюю библиотеку respect/validation с Slim или встроенные средства Laravel. Пример на Slim:
<?php
use Respect\Validation\Validator as v;
$app->post('/v1/users', function (Request $request, Response $response) {
$data = $request->getParsedBody();
$nameValidator = v::stringType()->notEmpty()->length(1, 100);
$emailValidator = v::email();
$errors = [];
if (!$nameValidator->validate($data['name'] ?? '')) {
$errors['name'] = 'Имя должно быть строкой от 1 до 100 символов';
}
if (!$emailValidator->validate($data['email'] ?? '')) {
$errors['email'] = 'Некорректный email';
}
if (!empty($errors)) {
$payload = json_encode(['errors' => $errors], JSON_UNESCAPED_UNICODE);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
// сохранение ...
return $response->withStatus(201);
});
Запрос с пустым именем вернёт:
{"errors":{"name":"Имя должно быть строкой от 1 до 100 символов"}}
Тестирование API v1
Как написать автоматические тесты для маршрутов?
С Slim используйте PHPUnit и встроенный Slim\Psr7\Factory\ServerRequestFactory:
<?php
use PHPUnit\Framework\TestCase;
use Slim\Psr7\Factory\ServerRequestFactory;
use Slim\Factory\AppFactory;
class UserApiTest extends TestCase
{
public function testGetUsersReturnsJson()
{
$app = AppFactory::create();
// настройка маршрутов как в основном приложении
$app->get('/v1/users', function ($request, $response) {
$response->getBody()->write(json_encode(['data' => []]));
return $response->withHeader('Content-Type', 'application/json');
});
$request = (new ServerRequestFactory)->createServerRequest('GET', '/v1/users');
$response = $app->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertStringContainsString('application/json', $response->getHeaderLine('Content-Type'));
}
}
Результат выполнения теста будет зелёным, если API отвечает корректно.