tprochazka/restful

Nette REST API bundle

dev-master 2018-09-06 22:56 UTC

This package is auto-updated.

Last update: 2024-09-07 16:03:47 UTC


README

marten-cz 分支 fork 来的,支持 PHP 7.2(如 Nette 中的 SmartObject 代替 Object)。并且修复了要求 nette/deprecated 依赖项的问题,由 Trejjam 提供。当原作者停止接收 pull request 时总是很遗憾。

Build Status

此存储库正在开发中。

内容

要求

Drahak/Restful 需要 PHP 版本 5.3.0 或更高版本。唯一的依赖项是 Nette 框架 2.0.x。Restful 也与我的 Drahak\OAuth2 提供程序兼容(见 使用 OAuth2 保护资源

安装与设置

最简单的方法是使用 Composer

$ composer require tprochazka/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_casecamelCase & PascalCase,这些值自动转换资源数组键。您可以编写自己的转换器。只需实现 Drahak\Restful\Resource\IConverter 接口,并使用 restful.converter 标记您的服务。
  • routes.generateAtStart:在 Router 开始时生成路由(仅适用于自动生成的路由,并且如果 Router 已通过 config.neon 设置,则为 true)
  • 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 资源路由。 REST API resource routes panel

示例用法

<?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/xmlapplication/jsonapplication/x-data-urlapplication/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 操作。具体来说,它是 Presenter:create 用于 POST 方法,Presenter:read 用于 GET,Presenter:update 用于 PUT 和 Presenter:delete 用于 DELETE。然后您的路由将看起来像这样

<?php
new CrudRoute('<module>/crud', 'MyResourcePresenter');

注意:第二个参数是元数据。您可以只定义 Presenter 而不是动作名称。这是因为动作名称将被来自动作字典的值替换([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 报头或通过在 URL 中添加查询参数 __method,则可以覆盖每个请求方法。

让我们看看关系

关系在 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表单及其验证。让我们在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(UnproccessableEntity)的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

}

提示:在使用此身份验证(以及标准事物,如用户的Identity)时请谨慎。记住,将REST API保持为无状态。从实用的角度来看,这不是一个好的方法,但它是最简单的方法。

安全身份验证

当第三方客户端连接时,您必须找到另一种方式来验证这些请求。SecuredAuthentication在很大程度上是答案。它基于发送带有私有密钥的哈希数据。由于数据已经被加密,因此它不依赖于SSL。认证过程如下

理解认证过程

  • 客户端:将请求时间戳附加到请求体中。
  • 客户端:使用hash_hmac(sha256算法)和私有密钥对所有数据进行哈希处理。然后将生成的哈希附加到请求作为X-HTTP-AUTH-TOKEN头(默认情况下)。
  • 客户端:将请求发送到服务器。
  • 服务器:接受客户端的请求并以相同的方式计算哈希(使用抽象模板类AuthenticationProcess)。
  • 服务器:比较客户端的哈希与之前步骤中生成的哈希。
  • 服务器:还检查请求时间戳并计算差异。如果它大于300(5分钟),则抛出异常。(这避免了称为重放攻击的事情)
  • 服务器:捕获抛出 AuthenticationProcess 的任何 SecurityException 并提供错误响应。

默认的 AuthenticationProcessNullAuthentication,因此所有请求都是未加密的。您可以使用 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 中使用它。要这样做,只需在 composer 中添加依赖项 "drahak/oauth2": "dev-master",然后使用 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 请求中,您将 API 资源作为 JavaScript 使用 HTML 中的标准 script 标签加载。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) 进行“网络化”。如果您在配置中将 jsonpKey 设置为 FALSENULL,则完全禁用所有 API 资源的 JSONP 模式。然后您可以手动触发它。只需将 IResource$contentType 属性设置为 IResource::JSONP

还请注意:如果启用此选项并且客户端将 jsonp 参数添加到查询字符串中,无论您将 $presenter->resource->contentType 设置为什么,它都会生成 JsonpResponse

使生活变得更美好的工具

API 请求过滤是常见的。这就是为什么再次执行它很无聊。Restful 提供了 RequestFilter,它会为您解析最常见的东西。在 ResourcePresenter 中,您可以在 $requestFilter 属性中找到 RequestFilter

分页器

通过将 offsetlimit 参数添加到查询字符串,您可以创建标准的 Nette Paginator。然后,您的 API 资源会通过 Link 头响应(其中 "最后一页" 部分 LinkX-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')的数组。

就是这样。祝您享受并希望您喜欢!