Версия 1 REST API на PHP: от проектирования до реализации

Раздел: Веб-разработка -> RESTful сервисы

Основной подход: использование микрофреймворка 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 отвечает корректно.

API v1 на PHP - comments

En
Api v1 php (php)