tomaj/nette-api

Nette api

2.11.0 2023-09-05 15:04 UTC

README

Nette 简单 API 库

Build Status Scrutinizer Code Quality Code Coverage Latest Stable Version

SensioLabsInsight

为什么选择 Nette-Api

此库为 Nette 框架提供开箱即用的 API 解决方案。您可以注册 API 端点并将其连接到指定的处理程序。您只需实现您自己的业务逻辑。库为您提供了 API 的授权、验证、速率限制和格式化服务。

安装

此库需要 PHP 7.1 或更高版本。

推荐安装方法是使用 Composer

composer require tomaj/nette-api

库符合 PSR-1PSR-2PSR-3PSR-4

Nette-API 的工作原理

首先,您必须为路由注册库表示器。在 config.neon 中添加此行

application:
  mapping:
    Api: Tomaj\NetteApi\Presenters\*Presenter

然后添加路由到您的 RouterFactory

$router[] = new Route('/api/v<version>/<package>[/<apiAction>][/<params>]', 'Api:Api:default');

如果您想使用 RESTful URL,您还需要另一个路由

$router[] = new Route('api/v<version>/<package>/<id>', [
    'presenter' => 'Api:Api',
    'action' => 'default',
    'id' => [
        Route::FILTER_IN => function ($id) {
            $_GET['id'] = $id;
            return $id;
        }
    ],
]);

之后,您只需将您的 API 处理程序注册到 apiDecider ApiDecider,注册 ApiLinkTomaj\NetteApi\Misc\IpDetector。这也可以通过 config.neon 完成

services:
  - Tomaj\NetteApi\Link\ApiLink
  - Tomaj\NetteApi\Misc\IpDetector
  apiDecider:
    factory: Tomaj\NetteApi\ApiDecider
    setup:
      - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\NoAuthorization())
      - addApi(\Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'users', 'send-email'), \App\MyApi\v1\Handlers\SendEmailHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization())

如示例所示,您可以注册任意数量的端点,并具有不同的配置。Nette-Api 从一开始就支持 API 版本控制。此示例将准备以下 API 调用

  1. http://yourapp/api/v1/users - 可通过 GET 获取
  2. http://yourapp/api/v1/users/send-email - 可通过 POST 获取

Nette-Api 的核心是处理程序。对于此示例,您需要实现两个类

  1. App\MyApi\v1\Handlers\UsersListingHandler
  2. App\MyApi\v1\Handlers\SendEmailHandler

这些处理程序实现接口 ApiHandlerInterface,但为了更方便的使用,您可以从 BaseHandler 扩展您的处理程序。当有人访问您的 API 时,这些处理程序将被触发,并将调用 handle() 方法。

namespace App\MyApi\v1\Handlers;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

class UsersListingHandler extends Basehandler
{
    private $userRepository;

    public function __construct(UsersRepository $userRepository)
    {
        parent::__construct();
        $this->userRepository = $userRepository;
    }

    public function handle(array $params): ResponseInterface
    {
        $users = [];
        foreach ($this->userRepository->all() as $user) {
            $users[] = $user->toArray();
        }
        return new JsonApiResponse(200, ['status' => 'ok', 'users' => $users]);
    }
}

此简单的处理程序使用了由 Nette 容器创建的 UsersRepository(因此您必须在 config.neon 中注册您的 App\MyApi\v1\Handlers\UsersListingHandler)。

高级用法(与 Fractal 结合使用)

Nette-Api 提供了与 Fractal 库的集成,用于格式化 API 响应。如果您想使用它,您必须从 BaseHandler 扩展您的处理程序,并且您的 Fractal 实例将可通过 $this->getFractal() 访问。

Fractal 的主要优势是将您的 API 的“视图”(例如将数据转换为 JSON 对象或 XML 或其他任何内容)分离出来。您还可以在其他转换中包含转换,以将其他对象包含到其他对象中。

使用 Fractal 的示例

  1. 您需要 Transformer
namespace App\MyApi\v1\Transformers;

use League\Fractal\TransformerAbstract;

class UserTransformer extends TransformerAbstract
{
    public function transform($user)
    {
        return [
            'id' => $user->id,
            'email' => $user->email,
            'name' => $user->name,
        ];
    }
}
  1. 这将您的处理程序
namespace App\MyApi\v1\Handlers;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

class UsersListingHandler extends Basehandler
{
    private $userTransformer;

    public function __construct(UserTransformer $userTransformer)
    {
        parent::__construct();
        $this->userTransformer = $userTransformer;
    }

    public function handle(array $params): ResponseInterface
    {
        $users = $this->useRepository->all(); 

        $resource = new Collection($users, $this->userTransformer);
        $result = $this->getFractal()->createData($resource)->toArray();

        return new JsonApiResponse(200, $result);
    }
}

我们建议您查看 Fractal 库。有关转换器、序列化器、分页等信息,库中有很多信息。这是一个非常好的库。

在 latte 中使用 ApiLink

首先,您必须在 config.neon 中注册过滤器

services:
    apiLink: Tomaj\NetteApi\Link\ApiLink()
    latte.latteFactory:
        setup:
            - addFilter(apiLink, [@apiLink, link])

注意:过滤器的名称必须是 apiLink,因为它在宏/扩展中使用。

对于 latte < 3.0 注册 latte 宏

latte:
    macros:
        - Tomaj\NetteApi\Link\ApiLinkMacro

对于 latte >= 3.0 注册 latte 扩展

latte:
    extensions:
        - Tomaj\NetteApi\Link\ApiLinkExtension

在 latte 文件中的使用

{apiLink $method, $version, $package, $apiAction, ['title' => 'My title', 'data-foo' => 'bar']}

端点输入

每个处理器可以描述所需的输入。可以是 GET 或 POST 参数,也可以是 COOKIES、原始 POST、JSON 或文件上传。您必须实现方法 params(),在该方法中,您必须返回包含参数的数组。这些参数在 API 控制台中用于生成表单。

用户详情示例

namespace App\MyApi\v1\Handlers;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\GetInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

class UsersDetailHandler extends Basehandler
{
    private $userRepository;

    public function __construct(UsersRepository $userRepository)
    {
        parent::__construct();
        $this->userRepository = $userRepository;
    }

    public function params(): array
    {
        return [
            (new GetInputParam('id'))->setRequired(),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        $user = $this->userRepository->find($params['id']);
        if (!$user) {
            return new JsonApiResponse(404, ['status' => 'error', 'message' => 'User not found']);
        }
        return new JsonApiResponse(200, ['status' => 'ok', 'user' => [
            'id' => $user->id,
            'email' => $user->email,
            'name' => $user->name,
        ]);
    }
}

输入类型

Nette-Api 提供了各种 InputParam 类型。您可以通过 GET、POST、COOKIES、FILES、RAW POST 数据或 JSON 发送参数。所有输入类型都可通过测试控制台访问。

这是支持输入类型的表格

输出

通过为您的处理器实现输出方法,您可以指定可能的输出列表(例如输出架构),这些将在响应发送给用户之前进行验证。如果没有设置输出,则不进行验证即发送响应给用户。

使用示例

public function outputs(): array
{
    $schema = [
        'type' => 'object',
        'properties' => [
            'name' => [
                'type' => 'string',
            ],
            'surname' => [
                'type' => 'string',
            ],
            'sex' => [
                'type' => 'string',
                'enum' => ['M', 'F'],
            ],
        ],
        'required' => ['name', 'surname'],
        'additionalProperties' => false,
    ];
    return [
        new JsonOutput(200, json_encode($schema)),
    ];
}

有关更多示例,请参阅 JSON schema 网页。请注意,Nette API 使用 justinrainbow/json-schema 来验证架构,并且此包仅支持 json schema draft 03 和 04。

安全性

使用 Nette-Api 保护您的 API 很容易。您必须实现自己的 授权(Tomaj\NetteApi\Authorization\ApiAuthorizationInterface)或使用预定义的实现,并将其作为第三个参数添加到 config.neon 中的 addApi() 方法中。

基本身份验证

基本身份验证是内置在 HTTP 协议中的简单身份验证方案。它包含用户名和密码。您可以定义任意多的用户名和密码对。但每个用户名只有一个密码。

services:
    apiDecider:
        factory: Tomaj\NetteApi\ApiDecider
        setup:
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BasicBasicAuthentication(['first-user': 'first-password', 'second-user': 'second-password']))

Bearer 令牌身份验证

对于使用少量令牌的简单 Bearer 令牌授权,您可以使用 StaticTokenRepository(Tomaj\NetteApi\Misc\StaticTokenRepository)。

services:
    staticTokenRepository: Tomaj\NetteApi\Misc\StaticTokenRepository(['dasfoihwet90hidsg': '*', 'asfoihweiohgwegi': '127.0.0.1'])

    apiDecider:
        factory: Tomaj\NetteApi\ApiDecider
        setup:
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization(@staticTokenRepository))

通过此注册,您将拥有可从任何地方通过带有 Authorisation HTTP 标头 Bearer dasfoihwet90hidsg 访问的 api /api/v1/users,或从 127.0.0.1 通过 Bearer asfoihweiohgwegi 访问。在 Nette-Api 中,如果您想为令牌指定 IP 限制,可以使用此模式

但实现您自己的 API 授权非常容易。

API 密钥

您还可以使用 API 密钥进行授权。API 密钥是在客户端进行 API 调用时提供的令牌。密钥可以发送在查询字符串、标头或 cookie 中。请参见以下示例

services:
    staticTokenRepository: Tomaj\NetteApi\Misc\StaticTokenRepository(['dasfoihwet90hidsg': '*', 'asfoihweiohgwegi': '127.0.0.1'])

    apiDecider:
        factory: Tomaj\NetteApi\ApiDecider
        setup:
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users', 'query'), Tomaj\NetteApi\Authorization\QueryApiKeyAuthentication('api_key', @staticTokenRepository))
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users', 'header'), Tomaj\NetteApi\Authorization\HeaderApiKeyAuthentication('X-API-KEY', @staticTokenRepository))
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users', 'cookie'), Tomaj\NetteApi\Authorization\CookieApiKeyAuthentication('api_key', @staticTokenRepository))

速率限制

此库提供简单的 API 速率限制接口。您需要做的就是像以下示例中那样实现此接口

use Nette\Application\Responses\TextResponse;
use Tomaj\NetteApi\RateLimit\RateLimitInterface;
use Tomaj\NetteApi\RateLimit\RateLimitResponse;

class MyRateLimit implements RateLimitInterface
{
    public function check(): ?RateLimitResponse
    {
        // do some logic here

        // example outputs:
        
        return null;    // no rate limit

        return new RateLimitResponse(60, 50);   // remains 50 of 60 hits
    
        return new RateLimitResponse(60, 0, 120);   // remains 0 of 60 hits, retry after 120 seconds

        return new RateLimitResponse(60, 0, 120, new TextResponse('My custom error message'));  // remains 0 of 60 hits, retry after 120 seconds, with custom TextResponse (default is Json response, see ApiPresenter::checkRateLimit())
    }
}

然后您必须将 API 注册到带有速率限制的 ApiDecider 中

services:
    apiDecider:
        factory: Tomaj\NetteApi\ApiDecider
        setup:
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), \Tomaj\NetteApi\Authorization\BearerTokenAuthorization(@staticTokenRepository), MyRateLimit())

JavaScript ajax 调用(CORS - 预检 OPTIONS 调用)

如果您需要从其他域通过 JavaScript ajax 调用 API,您需要为 预检调用中的 OPTIONS 方法 准备 API。Nette-api 已经为此情况做好了准备,并且您可以选择是否全局启用预检调用,或者您可以注册预定义的预检处理器。

全局启用 - 每个 API 端点都将可用于预检 OPTIONS 调用

services:
    apiDecider:
        factory: Tomaj\NetteApi\ApiDecider
        setup:
            - enableGlobalPreflight()
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())

或者您可以注册自定义的 OPTIONS 端点

services:
    apiDecider:
        factory: Tomaj\NetteApi\ApiDecider
        setup:
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('OPTIONS', 1, 'users'), \Tomaj\NetteApi\Handlers\CorsPreflightHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())
            - addApi(\Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), \App\MyApi\v1\Handlers\UsersListingHandler(), Tomaj\NetteApi\Authorization\NoAuthorization())
            

日志记录

如果你的API提供了有价值的信息,记录API访问是一种良好的实践。要启用记录,您需要实现具有接口的类 ApiLoggerInterface(Tomaj\NetteApi\Logger\ApiLoggerInterface),并在 config.neon 中将其注册为服务。它将在所有API请求执行后自动绑定并调用。

CORS安全

如果您需要使用JavaScript与API交互,则需要发送正确的CORS头。ApiPresenter 有属性可以设置这些头。默认情况下,API会发送带有值 ''* 的头 'Access-Control-Allow-Origin'。如果您需要更改它,可以将属性 $corsHeader 设置为以下值:

  1. 'auto' - 返回带有请求域的 'Access-Control-Allow-Origin' 头。这不够安全,但您可以通过AJAX从其他域名访问此API
  2. ''* - 发送带有 '*' 的头 - 如果您不使用具有 xhrFields: { withCredentials: true } 设置的 $.ajax 发送cookie到API,这将正常工作
  3. 'off' - 不会发送任何CORS头
  4. 其他 - 其他任何值都将发送到 'Access-Control-Allow-Origin' 头中

您可以在注册 ApiPresenter 时在 config.neon 中设置此属性

services:
  -
    factory: Tomaj\NetteApi\Presenters\ApiPresenter
    setup:
      - setCorsHeader('auto')

或者如果您扩展了 ApiPresenter,那么您可以在自己的展示器中设置它。

WEB控制台 - API测试器

Nette-Api 包含2个UI控件,可用于验证API。它将生成所有API调用的列表,并自动生成带有所有API参数的表单。

所有组件都生成bootstrap html,并且可以使用bootstrap css进行样式化

您必须在控制器中创建组件

use Nette\Application\UI\Presenter;
use Tomaj\NetteApi\ApiDecider;
use Tomaj\NetteApi\Component\ApiConsoleControl;
use Tomaj\NetteApi\Component\ApiListingControl;
use Tomaj\NetteApi\Link\ApiLink;

class MyPresenter extends Presenter
{
    private $apiDecider;

    private $apiLink;

    private $method;
    
    private $version;
    
    private $package;
    
    private $apiAction;

    public function __construct(ApiDecider $apiDecider, ApiLink $apiLink = null)
    {
        parent::__construct();
        $this->apiDecider = $apiDecider;
        $this->apiLink = $apiLink;
    }

    public function renderShow(string $method, int $version, string $package, ?string $apiAction = null): void
    {
        $this->method = $method;
        $this->version = $version;
        $this->package = $package;
        $this->apiAction = $apiAction;
    }

    protected function createComponentApiListing(): ApiListingControl
    {
        $apiListing = new ApiListingControl($this->apiDecider);
        $apiListing->onClick[] = function ($method, $version, $package, $apiAction) {
            $this->redirect('show', $method, $version, $package, $apiAction);
        };
        return $apiListing;
    }

    protected function createComponentApiConsole()
    {
        $api = $this->apiDecider->getApi($this->method, $this->version, $this->package, $this->apiAction);
        $apiConsole = new ApiConsoleControl($this->getHttpRequest(), $api->getEndpoint(), $api->getHandler(), $api->getAuthorization(), $this->apiLink);
        return $apiConsole;
    }
}

故障排除

如果您的Apache服务器在CGI或fastCGI脚本上运行,$_SERVER['HTTP_AUTHORIZATION'] 为空。您需要执行一些 mod_rewrite 巧技以将头通过CGI障碍传递,如下所示:

RewriteEngine on
RewriteRule .? - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]`

变更日志

有关最近更改的详细信息,请参阅 CHANGELOG

测试

$ composer test

贡献

有关详细信息,请参阅 CONTRIBUTINGCONDUCT

安全性

如果您发现任何安全相关的问题,请通过电子邮件 tomasmajer@gmail.com 而不是使用问题跟踪器。

许可证

MIT许可证(MIT)。有关更多信息,请参阅 许可证文件