mediagone/symfony-easy-api

使用普通的 Symfony 控制器快速构建 JSON API。

1.0.0 2024-09-25 13:20 UTC

This package is auto-updated.

Last update: 2024-09-25 14:22:14 UTC


README

Latest Version on Packagist Total Downloads Software License

此包提供了辅助类,可以轻松地从普通的 Symfony 控制器构建 Json API。
支持的功能

  • 单条和集合结果
  • 集合结果的分页
  • 开箱即用支持大多数有用的状态码(200、201、202、204、400、401、403、404、405、409、410、415、422、429、500 和 501)。

安装

此包需要 PHP 7.4+

将其作为 Composer 依赖项添加

$ composer require mediagone/symfony-easy-api

简介

此包提供了一些类来处理 API 请求并返回结构化的 JSON 响应

  • ApiPayload200Success
  • ApiPayload201Created
  • ApiPayload202Accepted
  • ApiPayload204NoContent
  • ApiPayloadError400BadRequest
  • ApiPayloadError401Unauthorized
  • ApiPayloadError403Forbidden
  • ApiPayloadError404NotFound
  • ApiPayloadError405MethodNotAllowed
  • ApiPayloadError409Conflict
  • ApiPayloadError410Gone
  • ApiPayloadError415UnsupportedMediaType
  • ApiPayloadError422UnprocessableEntity
  • ApiPayloadError429TooManyRequests
  • ApiPayloadError500ServerError
  • ApiPayloadError501NotImplemented

示例

构建 API 控制器的最简单方法是使用 EasyApi 类,并将控制器代码包裹在匿名函数中,以利用自动错误处理。

1. 实例 API 端点

EasyApi->response 方法接受任何可调用的参数,该参数返回一个 ApiPayload 实例。

use App\Thing\ThingRepository;
use Mediagone\Symfony\EasyApi\EasyApi;
use Mediagone\Symfony\EasyApi\Payloads\ApiPayload200Success;
use Mediagone\Symfony\EasyApi\Payloads\ApiPayloadError404NotFound;
use Symfony\Component\HttpFoundation\Response;

/**
 * @Route("/api/things/{thingId}", name="api_things", methods={"GET"})
 */
final class ApiEndpointController
{
    
    public function __invoke(int $thingId, EasyApi $easyApi, ThingRepository $thingRepository) : Response
    {
        return $easyApi->response(
            function() use ($thingId, $thingRepository) {
                // Any uncatched exception would be automatically converted into an ApiPayloadError500ServerError response's payload.
                if ($thingId < 1) {
                    throw new LogicException("Invalid `\$thingId` value ($thingId)");
                }
                
                $thing = $thingRepository->find($thingId);
                if ($thing === null) {
                    // Explicit "not found" error response's payload
                    return ApiPayloadError404NotFound::create('Thing not found (id: '.$thingId.')');
                }
                
                
                return ApiPayload200Success::createWithSingleResult($thing);
            }
        );
    }
    
}

在成功的情况下,前面的控制器将返回以下 JSON 对象

{
    "success": true,
    "status": "ok",
    "statusCode": 200,
    "payload": {
        "result": {
            "id": 1,
            "name": "First thing"
        }
    }
}

或一个 "未找到" 的响应

{
    "success": false,
    "status": "not_found",
    "statusCode": 404,
    "error": "not_found",
    "errorDescription": "Thing not found (id: 1)",
    "errorCode": 0
}

或一个 "服务器错误" 的响应

{
    "success": false,
    "status": "server_error",
    "statusCode": 500,
    "error": "server_error",
    "errorDescription": "Unexpected server error: Invalid `$thingId` value (-1)",
    "errorCode": 0
}

注意:errorCode 是 PHP 异常的内部错误代码(默认为 0)。您通常可以通过向构造函数传递一个额外的整数参数来定义它,例如 throw new LogicException("Invalid $thingId value ($thingId)", 1234);

2. 实例集合 API 端点

您还可以通过使用 ApiPayload200Success::createWithArrayResult 工厂方法返回多个结果

/**
 * @Route("/api/things", name="api_things", methods={"GET"})
 */
final class ApiEndpointController
{
    
    public function __invoke(EasyApi $easyApi, ThingRepository $thingRepository) : Response
    {
        return $easyApi->response(
            function() use ($thingRepository)
            {
                $things = $thingRepository->findAll();
                return ApiPayload200Success::createWithArrayResult($things);
            }
        );
    }
    
}

这将导致一个略有不同的 JSON 对象

{
    "success": true,
    "status": "ok",
    "statusCode": 200,
    "payload": {
        "results": [
            { "id": 1, "name": "First thing" },
            { "id": 2, "name": "Second thing" },
            { "id": 3, "name": "Third thing" }
        ],
        "resultsCount": 3,
        "resultsCountTotal": 3,
        "page": 1,
        "pageCount": 1
    }
}

3. 集合分页

当处理大量的数据库条目时,您可能希望分页结果以分块检索它们。
该包提供了 ApiPagination 类来帮助实现此功能。

它需要两个数据库查询:一个用于计算结果总数,另一个用于获取请求的结果

use Mediagone\Symfony\EasyApi\EasyApi;
use Mediagone\Symfony\EasyApi\Payloads\ApiPayload;
use Mediagone\Symfony\EasyApi\Payloads\ApiPayload200Success;
use Mediagone\Symfony\EasyApi\Request\ApiPagination;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/api/things/{requestedPage}', name:'api_things_list')]
public function __invoke(int $requestedPage = 1, ThingRepository $thingRepository): Response
{
    return $easyApi->response(static function () use ($requestedPage, $thingRepository) : ApiPayload {
        // Count the total number of Things in the db
        $thingsCount = $thingRepository->countAll();
        
        // Create a pagination object
        $pagination = ApiPagination::create($requestedPage, 5, $thingsCount);
        
        // Query the page's results
        $things = $thingRepository->findAllPaginated($pagination);
        
        return ApiPayload200Success::createWithArrayResult($things, $pagination);
    }
}

假设您的数据库中有 93 行,您正在请求第 2 页的 5 个结果,您将收到以下 JSON 响应

{
    "success": true,
    "status": "ok",
    "statusCode": 200,
    "payload": {
        "results": [
            { "id": 6, "name": "6th thing" },
            { "id": 7, "name": "7th thing" },
            { "id": 8, "name": "8th thing" },
            { "id": 9, "name": "9th thing" },
            { "id": 10, "name": "10th thing" }
        ],
        "resultsCount": 5,
        "resultsCountTotal": 93,
        "page": 2,
        "pageCount": 19
    }
}

许可

Symfony EasyAPI 使用 MIT 许可证。请参阅 LICENSE 文件。