drahak / restful
Nette REST API 扩展包
Requires
- php: >= 5.3.0
- nette/application: ~2.0|~3.0
- nette/bootstrap: ~2.0|~3.0
- nette/caching: ~2.0|~3.0
- nette/deprecated: ~2.0|~3.0
- nette/di: ~2.0|~3.0
- nette/http: ~2.0|~3.0
- nette/reflection: ~2.0|~3.0
- nette/robot-loader: ~2.0|~3.0
- nette/security: ~2.0|~3.0
- nette/utils: ~2.0|~3.0
Requires (Dev)
- drahak/oauth2: dev-master
- jakub-onderka/php-parallel-lint: ~0.8
- janmarek/mockista: 1.0.0
- nette/tester: ~1.3
This package is not auto-updated.
Last update: 2024-09-15 22:01:52 UTC
README
此仓库正在开发中。
内容
需求
Drahak/Restful 需要 PHP 版本 5.3.0 或更高。唯一的生产依赖项是 Nette 框架 2.0.x。Restful 还与我的 Drahak\OAuth2 提供者配合良好(参见 使用 OAuth2 保护您的资源)
安装与设置
最简单的方法是使用 Composer
$ composer require drahak/restful:@dev
然后在 bootstrap.php
中注册扩展(在创建容器之前)
Drahak\Restful\DI\RestfulExtension::install($configurator);
或者在 config.neon
中注册它
extensions: restful: Drahak\Restful\DI\RestfulExtension
Neon 配置
您可以在 config.neon
的 restful
部分中配置 Drahak\Restful 库
restful: convention: 'snake_case' cacheDir: '%tempDir%/cache' jsonpKey: 'jsonp' prettyPrintKey: 'pretty' routes: generateAtStart: FALSE prefix: resources module: 'RestApi' autoGenerated: TRUE panel: TRUE mappers: myMapper: contentType: 'multipart/form-data' class: \App\MyMapper security: privateKey: 'my-secret-api-key' requestTimeKey: 'timestamp' requestTimeout: 300
cacheDir
: 没什么好说的,只是存储缓存的目录jsonpKey
: 设置查询参数名称,启用 JSONP 封装模式。如果希望禁用它,请设置为 FALSE。prettyPrintKey
: API 默认以美化打印方式打印每个资源。您可以使用此查询参数来禁用它convention
: 资源数组键约定。目前支持 3 个值:snake_case
、camelCase
&PascalCase
,它们会自动转换资源数组键。您可以编写自己的转换器。只需实现Drahak\Restful\Resource\IConverter
接口并使用restful.converter
标签您的服务。routes.generateAtStart
: 在 Router 的开始时生成路由(仅针对自动生成的路由,并且如果是在 config.neon 中设置 Router)routes.prefix
: 遮罩资源路由的前缀(仅针对自动生成的路由)routes.module
: 资源路由的默认模块(仅针对自动生成的路由)routes.autoGenerated
: 如果为TRUE
,则库会自动从 Presenter 动作方法注解中生成资源路由(参见以下内容)routes.panel
: 如果为TRUE
,则资源路由面板将出现在您的 nette 调试栏中mappers
: 替换现有的映射器或为不同的内容类型添加新的映射器security.privateKey
: 用于哈希受保护请求的私钥security.requestTimeKey
: 请求体中的键,其中包含请求时间戳(参见以下内容 - 安全与认证)security.requestTimeout
: 最大请求时间戳年龄
提示:为您的资源使用 gzip 压缩。您可以在 neon 中简单地启用它
php: zlib.output_compression: yes
资源路由面板
默认情况下它是启用的,但你可以通过将restful.routes.panel
设置为FALSE
来禁用它。此面板显示所有REST API资源路由(确切地说,是默认路由列表中实现IResourceRouter
接口的所有路由)。这对于开发客户端应用程序的开发者来说很有用,因为他们可以将所有API资源路由放在一个地方。
示例用法
<?php namespace ResourcesModule; use Drahak\Restful\IResource; use Drahak\Restful\Application\UI\ResourcePresenter; /** * SamplePresenter resource * @package ResourcesModule * @author Drahomír Hanák */ class SamplePresenter extends ResourcePresenter { protected $typeMap = array( 'json' => IResource::JSON, 'xml' => IResource::XML ); /** * @GET sample[.<type xml|json>] */ public function actionContent($type = 'json') { $this->resource->title = 'REST API'; $this->resource->subtitle = ''; $this->sendResource($this->typeMap[$type]); } /** * @GET sample/detail */ public function actionDetail() { $this->resource->message = 'Hello world'; } }
资源输出由Accept
头部确定。库会检查头部中的application/xml
、application/json
、application/x-data-url
和application/www-form-urlencoded
,并按照Accept
头部的顺序保留。
注意:如果你使用带有MIME类型的第一参数调用$presenter->sendResource()
方法,API将只接受这个类型。
另外注意:有可用的注解@GET
、@POST
、@PUT
、@HEAD
、@DELETE
。这允许Drahak\Restful库为你生成API路由,因此你不需要手动生成。但这不是必须的!你可以使用IResourceRoute
或其默认实现(例如)来定义你的路由。
<?php use Drahak\Restful\Application\Routes\ResourceRoute; $anyRouteList[] = new ResourceRoute('sample[.<type xml|json>]', 'Resources:Sample:content', ResourceRoute::GET);
与Nette默认路由不同的是,这里只有一个额外的参数,即请求方法。这允许你为例如GET和POST方法生成相同的URL。你可以将此参数作为标志传递给路由,以便你可以组合更多的请求方法,如ResourceRoute::GET | ResourceRoute::POST
,以在同一路由上监听GET和POST请求方法。
你还可以为每个请求方法定义动作名称字典
<?php new ResourceRoute('myResourceName', array( 'presenter' => 'MyResourcePresenter', 'action' => array( ResourceRoute::GET => 'content', ResourceRoute::DELETE => 'delete' ) ), ResourceRoute::GET | ResourceRoute::DELETE);
简单的 CRUD 资源
这很好,但在许多情况下我只定义CRUD操作,如何更直观地做到这一点?使用CrudRoute
!这是ResourceRoute
的子类,为你预先定义了基本的CRUD操作。具体来说,对于POST方法,它是Presenter:create
,对于GET,是Presenter:read
,对于PUT,是Presenter:update
,对于DELETE,是Presenter:delete
。然后你的路由将看起来像这样
<?php new CrudRoute('<module>/crud', 'MyResourcePresenter');
注意第二个参数,元数据。你可以只定义Preseneter而不是动作名称。这是因为动作名称将由来自actionDictionary([CrudRoute::POST => 'create', CrudRoute::GET => 'read', CrudRoute::PUT => 'update', CrudRoute::DELETE => 'delete']
)的值替换,该值是ResourceRoute
的属性,因此即使是CrudRoute
,因为它是其子类。另外注意,我们不需要设置标志。默认标志设置为CrudRoute::CRUD
,因此该路由将匹配所有请求方法。
然后你可以简单地定义你的CRUD资源表示
<?php namespace ResourcesModule; /** * CRUD resource presenter * @package ResourcesModule * @author Drahomír Hanák */ class CrudPresenter extends BasePresenter { public function actionCreate() { $this->resource->action = 'Create'; } public function actionRead() { $this->resource->action = 'Read'; } public function actionUpdate() { $this->resource->action = 'Update'; } public function actionDelete() { $this->resource->action = 'Delete'; } }
注意:每个请求方法都可以被覆盖,如果你在请求中指定了X-HTTP-Method-Override
头部,或者通过将查询参数__method
添加到URL中。
存在关系
关系在RESTful服务中很常见,但在URL中如何处理它?我们的目标是这样的GET /articles/94/comments[/5]
,而括号中的ID可能是可选的。路由将如下所示
$router[] = new ResourceRoute('api/v1/articles/<id>/comments[/<commentId>]', array( 'presenter' => 'Articles', 'action' => array( IResourceRouter::GET => 'readComment', IResourceRouter::DELETE => 'deleteComment' ) ), IResourceRouter::GET | IResourceRouter::DELETE);
动作名称中的请求参数
相当长。因此,有一个选项可以将其通用化。现在它看起来像这样
$router[] = new ResourceRoute('api/v1/<presenter>/<id>/<relation>[/<relationId>]', array( 'presenter' => 'Articles', 'action' => array( IResourceRouter::GET => 'read<Relation>', IResourceRouter::DELETE => 'delete<Relation>' ) ), IResourceRouter::GET | IResourceRouter::DELETE);
更好,但仍然相当长。让我们再次使用CrudRoute
$router[] = new CrudRoute('api/v1/<presenter>/<id>/[<relation>[/<relationId>]]', 'Articles');
这是最短的方法。它之所以有效,是因为CrudRoute
中的动作字典基本上如下。
array( IResourceRouter::POST => 'create<Relation>', IResourceRouter::GET => 'read<Relation>', IResourceRouter::PUT => 'update<Relation>', IResourceRouter::DELETE => 'delete<Relation>' )
还可以看看这个单个路由的几个示例
GET api/v1/articles/94 => Articles:read
DELETE api/v1/articles/94 => Articles:delete
GET api/v1/articles/94/comments => Articles:readComments
GET api/v1/articles/94/comments/5 => Articles:readComments
DELETE api/v1/articles/94/comments/5 => Articles:deleteComments
POST api/v1/articles/94/comments => Articles:createComments
...
当然,你可以将多个参数添加到动作名称中,创建更长的关系。
注意:如果关系或动作名称中的任何其他参数不存在,它将被忽略,并使用不带参数的名称。
注意:动作名称中的参数 不区分大小写
访问输入数据
如果您想构建REST API,可能还需要访问所有请求方法(GET、POST、PUT、DELETE和HEAD)的查询输入数据。因此,库定义了输入解析器,它读取数据并将其解析为数组。数据可以从查询字符串或请求体中获取,并由IMapper
解析。首先,库会查找请求体。如果它不为空,它会检查Content-Type
头,并确定正确的映射器(例如,对于application/json
-> JsonMapper
等)。然后,如果请求体为空,则尝试获取POST数据,最后甚至获取URL查询数据。
<?php namespace ResourcesModule; /** * Sample resource * @package ResourcesModule * @author Drahomír Hanák */ class SamplePresenter extends BasePresenter { /** * @PUT <module>/sample */ public function actionUpdate() { $this->resource->message = isset($this->input->message) ? $this->input->message : 'no message'; } }
它的好处是您不必关心请求方法。Nette Drahak REST API库会为您选择正确的输入解析器,但如何处理它还是取决于您。有可用的InputIterator
,您可以在表示器中迭代输入,或者在自己的输入解析器中作为迭代器使用。
输入数据验证
访问输入数据的第一条规则:永远不要相信客户端!这真的非常重要,因为这是安全性的关键特性。那么如何正确处理呢?您可能已经熟悉Nette Forms及其验证。让我们在Restful中做同样的事情!您可以为每个输入数据字段定义验证规则。要获取字段(即Drahak\Restful\Validation\IField
),只需在Input
(在表示器中:$this->input
)上调用带有字段名称的field
方法。然后定义规则(几乎)就像在Nette中一样。
/** * SamplePresenter resource * @package Restful\Api * @author Drahomír Hanák */ class SamplePresenter extends BasePresenter { public function validateCreate() { $this->input->field('password') ->addRule(IValidator::MIN_LENGTH, NULL, 3) ->addRule(IValidator::PATTERN, 'Please add at least one number to password', '/.*[0-9].*/'); } public function actionCreate() { // some save data insertion } }
就是这样!它并不完全像Nette,但非常相似。至少是基本的公共接口。
注意:验证方法validateCreate
。这个新的生命周期方法validate<Action>()
将在每个动作方法action<Action>()
之前处理。它不是必需的,但使用它来定义一些验证规则或验证数据是个好主意。如果验证失败,则抛出包含代码HTT/1.1 422(UnprocessableEntity)的BadRequestException异常,该异常可以被错误表示器处理。
错误展示器
提供可读错误响应的最简单但功能强大的方法之一是使用$presenter->sendErrorResponse(Exception $e)
方法。最简单的错误表示器可能如下所示
<?php namespace Restful\Api; use Drahak\Restful\Application\UI\ResourcePresenter; /** * Base API ErrorPresenter * @package Restful\Api * @author Drahomír Hanák */ class ErrorPresenter extends ResourcePresenter { /** * Provide error to client * @param \Exception $exception */ public function actionDefault($exception) { $this->sendErrorResource($exception); } }
客户端可以像在正常的API资源中一样确定首选格式。实际上,它只是将异常数据添加到资源中,并将其发送到输出。
安全与认证
Restful提供了几种方法来保护您的资源
基本身份验证
这是一个非常基础但功能强大的保护资源的方法。它基于标准的Nette用户身份验证(如果用户未登录,则抛出安全异常,该异常提供给客户端),因此它适用于受信任的客户端(例如自己的客户端应用程序等)。由于这是常见的Restful,它包含SecuredResourcePresenter
作为ResourcePresenter
的子类,它已经为您处理了BasicAuthentication
。请参阅示例
use Drahak\Restful\Application\UI\SecuredResourcePresenter /** * My secured resource presenter * @author Drahomír Hanák */ class ArticlesPresenter extends SecuredResourcePresenter { // all my resources are protected and reachable only for logged user's // you can also add some Authorizator to check user rights }
提示:小心使用此身份验证(以及标准的东西,如用户身份)。请记住,REST API应该是无状态的。从实用主义的角度来看,这不是一个好的方法,但这是最简单的方法。
安全身份验证
当第三方客户端连接时,您必须找到另一种方法来验证这些请求。SecuredAuthentication
是更多或更少的答案。它基于发送带有私钥的散列数据。由于数据已经加密,它不依赖于SSL。认证过程如下
了解认证过程
- 客户端:将请求时间戳附加到请求体。
- 客户端:使用
hash_hmac
(sha256算法)和私钥对所有数据进行散列。然后将生成的散列附加到请求作为X-HTTP-AUTH-TOKEN
头(默认情况下)。 - 客户端:将请求发送到服务器。
- 服务器:接受客户端请求并像客户端一样计算哈希(使用抽象模板类
AuthenticationProcess
) - 服务器:将客户端的哈希与之前步骤中生成的哈希进行比较。
- 服务器:还会检查请求的时间戳并计算差异。如果大于300(5分钟),则抛出异常。(这避免了所谓的 重放攻击)
- 服务器:捕获由
AuthenticationProcess
抛出的任何SecurityException
并提供错误响应。
默认的 AuthenticationProcess
是 NullAuthentication
,因此所有请求都是未加密的。您可以使用 SecuredAuthentication
来保护您的资源。要做到这一点,只需将此认证过程设置到 restful.authentication
或 $presenter->authentication
中的 AuthenticationContext
。
<?php namespace ResourcesModule; use Drahak\Restful\Security\Process\SecuredAuthentication; /** * CRUD resource presenter * @package ResourcesModule * @author Drahomír Hanák */ class CrudPresenter extends BasePresenter { /** @var SecuredAuthentication */ private $securedAuthentication; /** * Inject secured authentication process * @param SecuredAuthentication $auth */ public function injectSecuredAuthentication(SecuredAuthentication $auth) { $this->securedAuthentication = $auth; } protected function startup() { parent::startup(); $this->authentication->setAuthProcess($this->securedAuthentication); } // your secured resource action }
切勿发送私钥!
使用 OAuth2 保护您的资源
如果您想使用 OAuth2 保护您的 API 资源,您需要一些 OAuth2 提供商。我已经为 Nette 框架实现了 OAuth2 提供商 包,因此您可以使用它与 Restful 一起使用。要做到这一点,只需将依赖项 "drahak/oauth2": "dev-master"
添加到您的 composer 中,然后使用 OAuth2Authentication
,它是 AuthenticationProcess
。如果您想使用任何其他 OAuth2 提供商,您可以编写自己的 AuthenticationProcess
。
<?php namespace Restful\Api; use Drahak\Restful\IResource; use Drahak\Restful\Security\Process\AuthenticationProcess; use Drahak\Restful\Security\Process\OAuth2Authentication; /** * CRUD resource presenter * @package Restful\Api * @author Drahomír Hanák */ class CrudPresenter extends BasePresenter { /** @var AuthenticationProcess */ private $authenticationProcess; /** * Inject authentication process * @param OAuth2Authentication $auth */ public function injectSecuredAuthentication(OAuth2Authentication $auth) { $this->authenticationProcess = $auth; } /** * Check presenter requirements * @param $element */ public function checkRequirements($element) { parent::checkRequirements($element); $this->authentication->setAuthProcess($this->authenticationProcess); } // ... }
注意:这只是一个资源服务器,因此它处理访问令牌授权。要生成访问令牌,您需要创建 OAuth2 展示者(资源所有者和授权服务器 - 请参阅 Drahak\OAuth2 文档)。
JSONP 支持
如果您想通过 JavaScript 在远程主机上访问您的 API 资源,您不能在 API 上进行常规的 AJAX 请求。因此,JSONP 是另一种方法。在 JSONP 请求中,您使用标准的 script
标签在 HTML 中加载您的 API 资源。API 将 JSON 字符串包装到回调函数参数中。这实际上很简单,但需要特别注意。例如,您无法访问响应头或状态码。您可以将这些头和状态码包装到所有资源中,但这并不适合可以访问头信息的常规 API 客户端。库允许您添加特殊的查询参数 jsonp
(名称取决于您的配置,这是默认值)。如果您通过 ?jsonp=callback
访问资源,API 会自动确定 JSONP 模式并将所有资源包装到以下 JavaScript 中
callback({ "response": { "yourResourceData": "here" }, "status": 200, "headers": { "X-Powered-By": "Nette framework", ... } })
注意:函数名称。这是来自 jsonp
查询参数的名称。此字符串通过 Nette\Utils\Strings::webalize(jsonp, NULL, FALSE)
进行“web化”。如果您将 jsonpKey
设置为 FALSE
或 NULL
,则完全禁用所有 API 资源的 JSONP 模式。然后您可以手动触发它。只需将 IResource
的 $contentType
属性设置为 IResource::JSONP
。
另外注意:如果此选项已启用且客户端将 jsonp
参数添加到查询字符串中,无论您将 $presenter->resource->contentType
设置为何,它都会生成 JsonpResponse
。
提高生活质量的工具
API 请求过滤是例行公事。这就是为什么再次执行它很无聊。Restful 提供了 RequestFilter
,它会为您解析最常见的事情。在 ResourcePresenter
中,您可以在 $requestFilter
属性中找到 RequestFilter
。
分页器
通过将 offset
和 limit
参数添加到查询字符串,您可以创建标准的 Nette Paginator
。然后,您的 API 资源以 Link
标头(其中“最后一页”部分和 X-Total-Count
标头仅提供如果您将总项目数设置到分页器中)进行响应。
Link: <URL_to_next_page>; rel="next",
<URL_to_last_page>; rel="last"
X-Total-Count: 1000
字段列表
如果您只想加载资源的一部分(例如,加载整个资源数据很昂贵),则应在查询参数中添加 fields
参数,并带有所需字段的列表(例如,fields=user_id,name,email
)。在 RequestFilter
中,您可以通过调用 getFieldsList()
方法来获取此列表(array('user_id', 'name', 'email')
)。
排序列表
如果您想要对资源提供的数据进行排序,您可能需要根据排序的属性。为了尽可能简化,您可以通过调用RequestFilter
方法的getSortList()
获取它,从sort
查询参数(如sort=name,-created_at
)中以array('name' => 'ASC', 'created_at' => 'DESC')
的形式获得。
就是这样。祝您玩得开心,希望您喜欢它!