Создание API на PHP с отдачей JSON: от простых решений до фреймворков
Основные подходы к реализации JSON API на PHP
Основное эффективное решение: микрофреймворк Slim 4
Slim Framework версии 4 является легковесным и современным инструментом для создания REST API. Он предоставляет минимальный набор функций, необходимых для маршрутизации, обработки запросов и формирования ответов. Установка производится через Composer:
composer require slim/slim:^4.0
Далее создается файл index.php, который служит точкой входа. Определяется маршрут для GET-запроса к /api/users, возвращается ответ в формате JSON:
<?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();
$app->get('/api/users', function (Request $request, Response $response) {
$users = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob']
];
$payload = json_encode($users);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();
Каждый шаг: подключение автозагрузчика, создание экземпляра приложения, определение маршрута с замыканием, формирование JSON-строки, запись в тело ответа и установка заголовка Content-Type. После запуска сервера (например, php -S localhost:8000) по адресу http://localhost:8000/api/users будет получен список пользователей.
Возможные проблемы и их решения:
- Отсутствие CORS-заголовков: добавьте middleware slim/cors или вручную установите заголовки в ответе.
- Необработанные ошибки: реализуйте кастомный обработчик ошибок через $app->addErrorMiddleware(true, true, true).
- Проблемы с автозагрузкой: убедитесь, что composer autoload подключен и файлы расположены согласно PSR-4.
Цели использования:
- Быстрое прототипирование API.
- Микросервисы с минимальными зависимостями.
- Проекты, не требующие полного MVC-фреймворка.
Как создать JSON API на чистом PHP без сторонних библиотек?
Иногда нет возможности использовать Composer или фреймворки. Тогда можно написать API на нативном PHP. Необходимо самостоятельно разобрать URI, определить маршрут и вернуть JSON.
<?php
// index.php
header('Content-Type: application/json');
$requestUri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET' && $requestUri === '/api/users') {
$users = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob']
];
echo json_encode($users);
exit;
}
http_response_code(404);
echo json_encode(['error' => 'Not found']);
Пояснение: устанавливается заголовок Content-Type, анализируется глобальный массив $_SERVER, выполняется простой роутинг, формируется JSON-ответ. При отсутствии маршрута возвращается 404.
Проблемы и решения:
- Отсутствие гибкости в роутинге: при увеличении числа маршрутов код становится нечитаемым. Решение: написать класс-роутер или использовать библиотеку.
- Нет поддержки PSR-7: все запросы и ответы приходится обрабатывать вручную, что повышает риск ошибок безопасности. Решение: подключить отдельные PSR-7 библиотеки (например, guzzlehttp/psr7).
- Ошибки валидации: необходимо вручную проверять параметры запроса. Решение: реализовать функции-валидаторы.
Цели использования:
- Обучение и понимание внутреннего устройства API.
- Простые однофайловые скрипты.
- Среда с ограниченными возможностями (например, shared hosting).
Как создать JSON API с помощью микрофреймворка Lumen?
Lumen - это легковесная версия Laravel, оптимизированная для API. Установка: composer create-project --prefer-dist laravel/lumen api.
// routes/web.php
$router->get('/api/users', function () use ($router) {
$users = [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob']];
return response()->json($users);
});
Lumen автоматически преобразует возвращаемый массив в JSON и устанавливает соответствующие заголовки. Если нужно более сложное поведение, используются контроллеры и Eloquent ORM.
Проблемы и решения:
- Необходимость включения фасадов (например, DB) в конфигурации. Решение: установить параметр app.php 'aliases' => [...] или использовать вспомогательные функции.
- CORS: требуется установка пакета barryvdh/laravel-cors и добавление middleware.
- Отсутствие некоторых функций Laravel (мощный artisan). Решение: для полноценного API лучше перейти на Laravel.
Цели использования:
- API-ориентированные проекты, где нужен минимум функций фреймворка.
- Быстрая разработка с возможностью масштабирования до Laravel.
Как построить JSON API на Laravel с использованием API Resources?
Laravel предоставляет богатый инструментарий для создания API. Создаем модель и контроллер с помощью artisan:
php artisan make:model User -mf
php artisan make:controller Api/UserController --resource
В контроллере можно использовать Resource классы для трансформации данных:
// UserController.php
public function index()
{
return UserResource::collection(User::all());
}
// UserResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at
];
}
Маршруты определяются в routes/api.php: Route::apiResource('users', 'Api\UserController');
Проблемы и решения:
- Избыточность для простых API: множество конфигурационных файлов, сервис-провайдеров. Решение: использовать Lumen или Slim.
- Производительность: Laravel тяжелее микрофреймворков. Решение: кэширование конфигурации (php artisan config:cache), использование PHP OPcache.
- Обработка ошибок: стандартные ответы ExceptionHandler можно кастомизировать.
Цели использования:
- Полноценные веб-приложения с API-частью.
- Проекты с богатой бизнес-логикой, авторизацией, очередями.
Как автоматически сгенерировать JSON API на Symfony с помощью API Platform?
API Platform - это набор инструментов для Symfony, который автоматически создает REST API и GraphQL на основе аннотаций Doctrine. Установка: composer require api-platform/core.
// src/Entity/User.php
#[ApiResource]
class User
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?string $name = null;
}
После настройки маршрутов (/api/users) API Platform генерирует все CRUD-операции, документацию Swagger, фильтры, пагинацию и т.д. Практически не требуется писать код контроллеров.
Проблемы и решения:
- Тяжеловесность: много зависимостей, долгая установка. Решение: использовать только для крупных проектов.
- Ограниченный контроль: сложно изменить поведение, отличное от стандартного. Решение: создание кастомных операций через композеры.
- Требуется знание Symfony: без понимания структуры сложно настраивать. Решение: начать с лёгких фреймворков.
Цели использования:
- API с большим количеством сущностей и сложными связями.
- Необходимость автоматической документации и клиентов.
Расширенные примеры кода
Дополнительные примеры демонстрируют более сложные сценарии, такие как обработка запросов с параметрами, аутентификация и пагинация.
<?php
// index.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type');
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = rtrim($uri, '/');
// Роутер
$routes = [
'GET' => [
'/api/users' => 'getUsers',
'/api/users/(\d+)' => 'getUser'
],
'POST' => [
'/api/users' => 'createUser'
]
];
$response = ['error' => 'Not Found'];
$status = 404;
foreach ($routes[$method] ?? [] as $pattern => $handler) {
$regex = '#^' . $pattern . '$#';
if (preg_match($regex, $uri, $matches)) {
array_shift($matches); // удаляем полное совпадение
$response = call_user_func_array($handler, $matches);
$status = 200;
break;
}
}
http_response_code($status);
echo json_encode($response);
function getUsers() {
return [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob']
];
}
function getUser($id) {
// Имитация поиска
return ['id' => (int)$id, 'name' => 'User' . $id];
}
function createUser() {
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['name'])) {
http_response_code(400);
return ['error' => 'Name is required'];
}
return ['id' => 3, 'name' => $input['name']];
}
$ curl -X POST http://localhost:8000/api/users -d '{"name":"Charlie"}' -H "Content-Type: application/json"
{"id":3,"name":"Charlie"}
Пояснение: реализован простой роутер с использованием регулярных выражений, поддержка CORS через заголовки, валидация входных данных через php://input и json_decode.
<?php
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
$app->addErrorMiddleware(true, true, true);
$secretKey = 'mySecretKey123';
// Middleware для проверки токена
$authMiddleware = function (Request $request, $handler) use ($secretKey) {
$authHeader = $request->getHeaderLine('Authorization');
if (empty($authHeader) || !str_starts_with($authHeader, 'Bearer ')) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode(['error' => 'Unauthorized']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
$token = substr($authHeader, 7);
try {
$decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
$request = $request->withAttribute('userId', $decoded->sub);
} catch (\Exception $e) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode(['error' => 'Invalid token']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
return $handler->handle($request);
};
// Маршрут для получения токена (логин)
$app->post('/api/login', function (Request $request, Response $response) use ($secretKey) {
$body = $request->getParsedBody();
$username = $body['username'] ?? '';
$password = $body['password'] ?? '';
// Проверка учетных данных (имитация)
if ($username === 'admin' && $password === '123') {
$payload = [
'iss' => 'myapp',
'sub' => 1,
'iat' => time(),
'exp' => time() + 3600
];
$token = JWT::encode($payload, $secretKey, 'HS256');
$response->getBody()->write(json_encode(['token' => $token]));
return $response->withHeader('Content-Type', 'application/json');
}
$response->getBody()->write(json_encode(['error' => 'Invalid credentials']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
});
// Защищенный маршрут
$app->get('/api/profile', function (Request $request, Response $response) {
$userId = $request->getAttribute('userId');
$response->getBody()->write(json_encode(['id' => $userId, 'name' => 'Admin']));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
$app->run();
$ curl -X POST http://localhost:8000/api/login -d "username=admin&password=123"
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."}
$ curl -H "Authorization: Bearer eyJ0eXAi..." http://localhost:8000/api/profile
{"id":1,"name":"Admin"}
Пояснение: используется библиотека firebase/php-jwt. Создан middleware для проверки Bearer-токена. Маршрут /api/login выдает токен после успешной аутентификации. Защищенный маршрут /api/profile требует валидного токена.
// routes/api.php
Route::get('/users', function (Request $request) {
$query = App\Models\User::query();
if ($request->has('name')) {
$query->where('name', 'like', '%' . $request->name . '%');
}
if ($request->has('email')) {
$query->where('email', $request->email);
}
$perPage = $request->get('per_page', 15);
$users = $query->paginate($perPage);
return UserResource::collection($users);
});
// UserResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
$ curl "http://localhost/api/users?name=Ali&per_page=5"
{
"data": [
{"id":1,"name":"Alice","email":"alice@example.com"},
{"id":21,"name":"Alison","email":"alison@example.com"}
],
"links": {
"first": "http://localhost/api/users?name=Ali&page=1",
"last": "http://localhost/api/users?name=Ali&page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://localhost/api/users",
"per_page": 5,
"to": 1,
"total": 2
}
}
Пояснение: в Laravel используется Query Builder с условными фильтрами. Метод paginate() автоматически добавляет пагинацию. UserResource трансформирует модели. Результат содержит метаданные пагинации.