创建一个简单的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头和状态也很重要。

namespace Application\Web;
class Response extends AbstractHttp
{

  public function __construct(Request $request = NULL, 
                              $status = NULL, $contentType = NULL)
  {
    if ($request) {
      $this->uri = $request->getUri();
      $this->data = $request->getData();
      $this->method = $request->getMethod();
      $this->cookies = $request->getCookies();
      $this->setTransport();
    }
    $this->processHeaders($contentType);
    if ($status) {
      $this->setStatus($status);
    }
  }
  protected function processHeaders($contentType)
  {
    if (!$contentType) {
      $this->setHeaderByKey(self::HEADER_CONTENT_TYPE, 
        self::CONTENT_TYPE_JSON);
    } else {
      $this->setHeaderByKey(self::HEADER_CONTENT_TYPE, 
        $contentType);
    }
  }
  public function setStatus($status)
  {
    $this->status = $status;
  }
  public function getStatus()
  {
    return $this->status;
  }
}

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

注意使用PHP 7的组使用语法。

use Application\Web\ { Request,Response,Received }
namespace Application\Web\Rest;
use Application\Web\ { Request, Response, Received };
class Server
{
  protected $api;
  public function __construct(ApiInterface $api)
  {
    $this->api = $api;
  }

4. 接下来,我们定义一个listen()方法,作为请求的目标。服务器实现的核心就是这行代码。

$jsonData = json_decode(file_get_contents('php://input'),true);

5. 这将捕获原始输入,假定为JSON格式。

public function listen()
{
  $request  = new Request();
  $response = new Response($request);
  $getPost  = $_REQUEST ?? array();
  $jsonData = json_decode(
    file_get_contents('php://input'),true);
  $jsonData = $jsonData ?? array();
  $request->setData(array_merge($getPost,$jsonData));

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

if (!$this->api->authenticate($request)) {
    $response->setStatus(Request::STATUS_401);
    echo $this->api::ERROR;
    exit;
}

6. 然后,我们将API方法映射到主要的HTTP方法GETPUTPOSTDELETE

$id = $request->getData()[$this->api::ID_FIELD] ?? NULL;
switch (strtoupper($request->getMethod())) {
  case Request::METHOD_POST :
    $this->api->post($request, $response);
    break;
  case Request::METHOD_PUT :
    $this->api->put($request, $response);
    break;
  case Request::METHOD_DELETE :
    $this->api->delete($request, $response);
    break;
  case Request::METHOD_GET :
  default :
    // return all if no params
  $this->api->get($request, $response);
}

7. 最后,我们将响应打包并发送出去

  $this->processResponse($response);
  echo json_encode($response->getData());
}

8. processResponse()方法设置了头文件,并确保结果被打包为Application\Web\Response对象。

protected function processResponse($response)
{
  if ($response->getHeaders()) {
    foreach ($response->getHeaders() as $key => $value) {
      header($key . ': ' . $value, TRUE, 
             $response->getStatus());
    }
  }        
  header(Request::HEADER_CONTENT_TYPE 
  . ': ' . Request::CONTENT_TYPE_JSON, TRUE);
  if ($response->getCookies()) {
    foreach ($response->getCookies() as $key => $value) {
      setcookie($key, $value);
    }
  }
}

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

namespace Application\Web\Rest;
use Application\Web\ { Request, Response };
abstract class AbstractApi implements ApiInterface
{
  const TOKEN_BYTE_SIZE  = 16;
  protected $registeredKeys;
  abstract public function get(Request $request, 
                               Response $response);
  abstract public function put(Request $request, 
                               Response $response);
  abstract public function post(Request $request, 
                                Response $response);
  abstract public function delete(Request $request, 
                                  Response $response);
  abstract public function authenticate(Request $request);
  public function __construct($registeredKeys, $tokenField)
  {
    $this->registeredKeys = $registeredKeys;
  }
  public static function generateToken()
  {
    return bin2hex(random_bytes(self::TOKEN_BYTE_SIZE));    
  }
}

10. 我们还定义了一个相应的接口,可以用于架构和设计的目的,以及代码开发控制。

namespace Application\Web\Rest;
use Application\Web\ { Request, Response };
interface ApiInterface
{
  public function get(Request $request, Response $response);
  public function put(Request $request, Response $response);
  public function post(Request $request, Response $response);
  public function delete(Request $request, Response $response);
  public function authenticate(Request $request);
}

11. 这里,我们介绍一个基于AbstractApi的示例API。这个类利用了第5章《与数据库的交互》中定义的数据库类。

namespace Application\Web\Rest;
use Application\Web\ { Request, Response, Received };
use Application\Entity\Customer;
use Application\Database\ { Connection, CustomerService };

class CustomerApi extends AbstractApi
{
  const ERROR = 'ERROR';
  const ERROR_NOT_FOUND = 'ERROR: Not Found';
  const SUCCESS_UPDATE = 'SUCCESS: update succeeded';
  const SUCCESS_DELETE = 'SUCCESS: delete succeeded';
  const ID_FIELD = 'id';      // field name of primary key
  const TOKEN_FIELD = 'token';  // field used for authentication
  const LIMIT_FIELD = 'limit';
  const OFFSET_FIELD = 'offset';
  const DEFAULT_LIMIT = 20;
  const DEFAULT_OFFSET = 0;
      
  protected $service;
      
  public function __construct($registeredKeys, 
                              $dbparams, $tokenField = NULL)
  {
    parent::__construct($registeredKeys, $tokenField);
    $this->service = new CustomerService(
      new Connection($dbparams));
  }

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

public function get(Request $request, Response $response)
{
  $result = array();
  $id = $request->getDataByKey(self::ID_FIELD) ?? 0;
  if ($id > 0) {
      $result = $this->service->
        fetchById($id)->entityToArray();  
  } else {
    $limit  = $request->getDataByKey(self::LIMIT_FIELD) 
      ?? self::DEFAULT_LIMIT;
    $offset = $request->getDataByKey(self::OFFSET_FIELD) 
      ?? self::DEFAULT_OFFSET;
    $result = [];
    $fetch = $this->service->fetchAll($limit, $offset);
    foreach ($fetch as $row) {
      $result[] = $row;
    }
  }
  if ($result) {
      $response->setData($result);
      $response->setStatus(Request::STATUS_200);
  } else {
      $response->setData([self::ERROR_NOT_FOUND]);
      $response->setStatus(Request::STATUS_500);
  }
}

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

public function put(Request $request, Response $response)
{
  $cust = Customer::arrayToEntity($request->getData(), 
                                  new Customer());
  if ($newCust = $this->service->save($cust)) {
      $response->setData(['success' => self::SUCCESS_UPDATE, 
                          'id' => $newCust->getId()]);
      $response->setStatus(Request::STATUS_200);
  } else {
      $response->setData([self::ERROR]);
      $response->setStatus(Request::STATUS_500);
  }      
}

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

public function post(Request $request, Response $response)
{
  $id = $request->getDataByKey(self::ID_FIELD) ?? 0;
  $reqData = $request->getData();
  $custData = $this->service->
    fetchById($id)->entityToArray();
  $updateData = array_merge($custData, $reqData);
  $updateCust = Customer::arrayToEntity($updateData, 
  new Customer());
  if ($this->service->save($updateCust)) {
      $response->setData(['success' => self::SUCCESS_UPDATE, 
                          'id' => $updateCust->getId()]);
      $response->setStatus(Request::STATUS_200);
  } else {
      $response->setData([self::ERROR]);
      $response->setStatus(Request::STATUS_500);
  }      
}

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

public function delete(Request $request, Response $response)
{
  $id = $request->getDataByKey(self::ID_FIELD) ?? 0;
  $cust = $this->service->fetchById($id);
  if ($cust && $this->service->remove($cust)) {
      $response->setData(['success' => self::SUCCESS_DELETE, 
                          'id' => $id]);
      $response->setStatus(Request::STATUS_200);
  } else {
      $response->setData([self::ERROR_NOT_FOUND]);
      $response->setStatus(Request::STATUS_500);
  }
}

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

public function authenticate(Request $request)
{
  $authToken = $request->getDataByKey(self::TOKEN_FIELD) 
    ?? FALSE;
  if (in_array($authToken, $this->registeredKeys, TRUE)) {
      return TRUE;
  } else {
      return FALSE;
  }
}
}

如何运行...

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

  • 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服务器。

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

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

php -S localhost:8080 chap_07_simple_rest_server.php

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

<?php return '79e9b5211bbf2458a4085707ea378129';

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

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

更多...

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

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

最后更新于