创建一个简单的REST服务器

在实现REST服务器时,有几个注意事项。这三个问题的答案将让你定义你的REST服务。

  • 如何捕获原始请求?
  • 你想发布什么应用编程接口(API)?
  • 你打算如何将HTTP动词(例如,GET、PUT、POST和DELETE)映射到API方法?

如何做…

1.我们将通过在之前的 《创建一个简单的REST客户端 》中定义的请求和响应类的基础上实现我们的REST服务器。回顾之前事例中讨论的类,包括以下内容。

  • Application\Web\AbstractHttp
  • Application\Web\Request
  • Application\Web\Received

2.我们还需要在AbstractHttp的基础上,定义一个正式的Application\Web\Response响应类。这个类和其他类的主要区别在于它接受Application\Web\Request的实例作为参数。主要工作在__construct()方法中完成。设置Content-Type头和状态也很重要。

  1. namespace Application\Web;
  2. class Response extends AbstractHttp
  3. {
  4. public function __construct(Request $request = NULL,
  5. $status = NULL, $contentType = NULL)
  6. {
  7. if ($request) {
  8. $this->uri = $request->getUri();
  9. $this->data = $request->getData();
  10. $this->method = $request->getMethod();
  11. $this->cookies = $request->getCookies();
  12. $this->setTransport();
  13. }
  14. $this->processHeaders($contentType);
  15. if ($status) {
  16. $this->setStatus($status);
  17. }
  18. }
  19. protected function processHeaders($contentType)
  20. {
  21. if (!$contentType) {
  22. $this->setHeaderByKey(self::HEADER_CONTENT_TYPE,
  23. self::CONTENT_TYPE_JSON);
  24. } else {
  25. $this->setHeaderByKey(self::HEADER_CONTENT_TYPE,
  26. $contentType);
  27. }
  28. }
  29. public function setStatus($status)
  30. {
  31. $this->status = $status;
  32. }
  33. public function getStatus()
  34. {
  35. return $this->status;
  36. }
  37. }

3.我们现在可以定义Application\WebRest\Server类。你可能会惊讶于它的简单程度。真正的工作是在相关的API类中完成的。

{% hint style=”info” %} 注意使用PHP 7的组使用语法。

  1. use Application\Web\ { Request,Response,Received }

{% endhint %}

  1. namespace Application\Web\Rest;
  2. use Application\Web\ { Request, Response, Received };
  3. class Server
  4. {
  5. protected $api;
  6. public function __construct(ApiInterface $api)
  7. {
  8. $this->api = $api;
  9. }
  1. 接下来,我们定义一个listen()方法,作为请求的目标。服务器实现的核心就是这行代码。
  1. $jsonData = json_decode(file_get_contents('php://input'),true);
  1. 这将捕获原始输入,假定为JSON格式。
  1. public function listen()
  2. {
  3. $request = new Request();
  4. $response = new Response($request);
  5. $getPost = $_REQUEST ?? array();
  6. $jsonData = json_decode(
  7. file_get_contents('php://input'),true);
  8. $jsonData = $jsonData ?? array();
  9. $request->setData(array_merge($getPost,$jsonData));

{% hint style=”info” %} 我们还增加了一项认证条款。否则,任何人都可以提出请求并获得潜在的敏感数据。你会注意到,我们并没有让服务器类来执行认证,而是让API类来执行。

  1. if (!$this->api->authenticate($request)) {
  2. $response->setStatus(Request::STATUS_401);
  3. echo $this->api::ERROR;
  4. exit;
  5. }

{% endhint %}

  1. 然后,我们将API方法映射到主要的HTTP方法GETPUTPOSTDELETE
  1. $id = $request->getData()[$this->api::ID_FIELD] ?? NULL;
  2. switch (strtoupper($request->getMethod())) {
  3. case Request::METHOD_POST :
  4. $this->api->post($request, $response);
  5. break;
  6. case Request::METHOD_PUT :
  7. $this->api->put($request, $response);
  8. break;
  9. case Request::METHOD_DELETE :
  10. $this->api->delete($request, $response);
  11. break;
  12. case Request::METHOD_GET :
  13. default :
  14. // return all if no params
  15. $this->api->get($request, $response);
  16. }
  1. 最后,我们将响应打包并发送出去
  1. $this->processResponse($response);
  2. echo json_encode($response->getData());
  3. }
  1. processResponse()方法设置了头文件,并确保结果被打包为Application\Web\Response对象。
  1. protected function processResponse($response)
  2. {
  3. if ($response->getHeaders()) {
  4. foreach ($response->getHeaders() as $key => $value) {
  5. header($key . ': ' . $value, TRUE,
  6. $response->getStatus());
  7. }
  8. }
  9. header(Request::HEADER_CONTENT_TYPE
  10. . ': ' . Request::CONTENT_TYPE_JSON, TRUE);
  11. if ($response->getCookies()) {
  12. foreach ($response->getCookies() as $key => $value) {
  13. setcookie($key, $value);
  14. }
  15. }
  16. }

9.如前所述,真正的工作是由API类完成的。我们首先定义一个抽象类,确保主要的方法get()put()等被表示出来,并且所有这些方法都接受请求和响应对象作为参数。你可能会注意到,我们添加了一个 generateToken() 方法,使用 PHP 7 random_bytes() 函数来生成一个真正随机的 16 字节随机数。

  1. namespace Application\Web\Rest;
  2. use Application\Web\ { Request, Response };
  3. abstract class AbstractApi implements ApiInterface
  4. {
  5. const TOKEN_BYTE_SIZE = 16;
  6. protected $registeredKeys;
  7. abstract public function get(Request $request,
  8. Response $response);
  9. abstract public function put(Request $request,
  10. Response $response);
  11. abstract public function post(Request $request,
  12. Response $response);
  13. abstract public function delete(Request $request,
  14. Response $response);
  15. abstract public function authenticate(Request $request);
  16. public function __construct($registeredKeys, $tokenField)
  17. {
  18. $this->registeredKeys = $registeredKeys;
  19. }
  20. public static function generateToken()
  21. {
  22. return bin2hex(random_bytes(self::TOKEN_BYTE_SIZE));
  23. }
  24. }
  1. 我们还定义了一个相应的接口,可以用于架构和设计的目的,以及代码开发控制。
  1. namespace Application\Web\Rest;
  2. use Application\Web\ { Request, Response };
  3. interface ApiInterface
  4. {
  5. public function get(Request $request, Response $response);
  6. public function put(Request $request, Response $response);
  7. public function post(Request $request, Response $response);
  8. public function delete(Request $request, Response $response);
  9. public function authenticate(Request $request);
  10. }
  1. 这里,我们介绍一个基于AbstractApi的示例API。这个类利用了第5章《与数据库的交互》中定义的数据库类。
  1. namespace Application\Web\Rest;
  2. use Application\Web\ { Request, Response, Received };
  3. use Application\Entity\Customer;
  4. use Application\Database\ { Connection, CustomerService };
  5. class CustomerApi extends AbstractApi
  6. {
  7. const ERROR = 'ERROR';
  8. const ERROR_NOT_FOUND = 'ERROR: Not Found';
  9. const SUCCESS_UPDATE = 'SUCCESS: update succeeded';
  10. const SUCCESS_DELETE = 'SUCCESS: delete succeeded';
  11. const ID_FIELD = 'id'; // field name of primary key
  12. const TOKEN_FIELD = 'token'; // field used for authentication
  13. const LIMIT_FIELD = 'limit';
  14. const OFFSET_FIELD = 'offset';
  15. const DEFAULT_LIMIT = 20;
  16. const DEFAULT_OFFSET = 0;
  17. protected $service;
  18. public function __construct($registeredKeys,
  19. $dbparams, $tokenField = NULL)
  20. {
  21. parent::__construct($registeredKeys, $tokenField);
  22. $this->service = new CustomerService(
  23. new Connection($dbparams));
  24. }

12.所有方法都接收请求和响应作为参数。你会注意到使用getDataByKey()来检索数据项。实际的数据库交互是由服务类执行的。你可能还会注意到,在所有情况下,我们都设置了一个HTTP状态码来通知客户端成功或失败。在get()的情况下,我们寻找一个ID参数。如果收到,我们只传递单个客户的信息。否则,我们使用limitoffset来传递所有客户的列表。

  1. public function get(Request $request, Response $response)
  2. {
  3. $result = array();
  4. $id = $request->getDataByKey(self::ID_FIELD) ?? 0;
  5. if ($id > 0) {
  6. $result = $this->service->
  7. fetchById($id)->entityToArray();
  8. } else {
  9. $limit = $request->getDataByKey(self::LIMIT_FIELD)
  10. ?? self::DEFAULT_LIMIT;
  11. $offset = $request->getDataByKey(self::OFFSET_FIELD)
  12. ?? self::DEFAULT_OFFSET;
  13. $result = [];
  14. $fetch = $this->service->fetchAll($limit, $offset);
  15. foreach ($fetch as $row) {
  16. $result[] = $row;
  17. }
  18. }
  19. if ($result) {
  20. $response->setData($result);
  21. $response->setStatus(Request::STATUS_200);
  22. } else {
  23. $response->setData([self::ERROR_NOT_FOUND]);
  24. $response->setStatus(Request::STATUS_500);
  25. }
  26. }

13.put()方法用于插入客户数据。

  1. public function put(Request $request, Response $response)
  2. {
  3. $cust = Customer::arrayToEntity($request->getData(),
  4. new Customer());
  5. if ($newCust = $this->service->save($cust)) {
  6. $response->setData(['success' => self::SUCCESS_UPDATE,
  7. 'id' => $newCust->getId()]);
  8. $response->setStatus(Request::STATUS_200);
  9. } else {
  10. $response->setData([self::ERROR]);
  11. $response->setStatus(Request::STATUS_500);
  12. }
  13. }

14.post()方法用于更新现有的客户条目。

  1. public function post(Request $request, Response $response)
  2. {
  3. $id = $request->getDataByKey(self::ID_FIELD) ?? 0;
  4. $reqData = $request->getData();
  5. $custData = $this->service->
  6. fetchById($id)->entityToArray();
  7. $updateData = array_merge($custData, $reqData);
  8. $updateCust = Customer::arrayToEntity($updateData,
  9. new Customer());
  10. if ($this->service->save($updateCust)) {
  11. $response->setData(['success' => self::SUCCESS_UPDATE,
  12. 'id' => $updateCust->getId()]);
  13. $response->setStatus(Request::STATUS_200);
  14. } else {
  15. $response->setData([self::ERROR]);
  16. $response->setStatus(Request::STATUS_500);
  17. }
  18. }

15.顾名思义,delete()删除客户条目。

  1. public function delete(Request $request, Response $response)
  2. {
  3. $id = $request->getDataByKey(self::ID_FIELD) ?? 0;
  4. $cust = $this->service->fetchById($id);
  5. if ($cust && $this->service->remove($cust)) {
  6. $response->setData(['success' => self::SUCCESS_DELETE,
  7. 'id' => $id]);
  8. $response->setStatus(Request::STATUS_200);
  9. } else {
  10. $response->setData([self::ERROR_NOT_FOUND]);
  11. $response->setStatus(Request::STATUS_500);
  12. }
  13. }

16.最后,我们定义了authenticate(),在这个例子中,该方法作为底层机制来保护API的使用。

  1. public function authenticate(Request $request)
  2. {
  3. $authToken = $request->getDataByKey(self::TOKEN_FIELD)
  4. ?? FALSE;
  5. if (in_array($authToken, $this->registeredKeys, TRUE)) {
  6. return TRUE;
  7. } else {
  8. return FALSE;
  9. }
  10. }
  11. }

如何运行…

定义以下类,这在前面的事例中已经讨论过。

  • Application\Web\AbstractHttp
  • Application\Web\Request
  • Application\Web\Received

然后,你可以定义以下类,在本事例中描述,总结在这个表中。

Class Application\Web\* 在这些步骤中讨论
Response 2
Rest\Server 3 - 8
Rest\AbstractApi 9
Rest\ApiInterface 10
Rest\CustomerApi 11 - 16

现在你可以自由地开发你自己的API类了。然而,如果你选择按照Application\Web\Rest\CustomerApi的说明,你还需要确保实现这些类,在第5章 《与数据库的交互》中有所涉及。

  • Application\Entity\Customer
  • Application\Database\Connection
  • Application\Database\CustomerService

现在你可以定义一个chap_07_simple_rest_server.php脚本来调用REST服务器。

  1. <?php
  2. $dbParams = include __DIR__ . '/../../config/db.config.php';
  3. require __DIR__ . '/../Application/Autoload/Loader.php';
  4. Application\Autoload\Loader::init(__DIR__ . '/..');
  5. use Application\Web\Rest\Server;
  6. use Application\Web\Rest\CustomerApi;
  7. $apiKey = include __DIR__ . '/api_key.php';
  8. $server = new Server(new CustomerApi([$apiKey], $dbParams, 'id'));
  9. $server->listen();

然后你可以使用内置的PHP 7开发服务器来监听8080端口的REST请求。

  1. php -S localhost:8080 chap_07_simple_rest_server.php

要测试你的API,使用Application\Web\Rest\AbstractApi::generateToken()方法来生成一个认证令牌,你可以把它放在api_key.php文件中,就像这样。

  1. <?php return '79e9b5211bbf2458a4085707ea378129';

然后,你可以使用一个通用的API客户端(如前面的事例中描述的客户端),或者一个浏览器插件,如Chao Zhou的RESTClient(更多信息见http://restclient.net/)来生成示例请求。确保你的请求包含了令牌,否则定义的API会拒绝该请求。

下面是一个ID 为 1的POST请求的例子,它将余额字段设置为888888。

创建一个简单的REST服务器 - 图1

更多…

有很多库可以帮助你实现一个REST服务器。我最喜欢的一个例子是在一个文件中实现REST服务器:https://www.leaseweb.com/labs/2015/10/creating-a-simple-rest-api-in-php/

各种框架,如CodeIgniter和Zend Framework,也有REST服务器的实现。