创建一个简单的REST客户端

REST客户端使用超文本传输协议(HTTP)来生成对外部Web服务的请求。通过改变HTTP方法,我们可以使外部服务执行不同的操作。虽然有不少方法(或动词)可以使用,但我们将只关注GETPOST。本文中,我们将使用Adapter软件设计模式来介绍实现REST客户端的两种不同方式。

如何做…

1.在我们定义REST客户端适配器之前,我们需要定义通用类来表示请求和响应信息。首先,我们将从一个抽象类开始,该类具有请求或响应所需的方法和属性。

  1. namespace Application\Web;
  2. class AbstractHttp
  3. {
  1. 接下来,我们定义代表HTTP信息的类常量。
  1. const METHOD_GET = 'GET';
  2. const METHOD_POST = 'POST';
  3. const METHOD_PUT = 'PUT';
  4. const METHOD_DELETE = 'DELETE';
  5. const CONTENT_TYPE_HTML = 'text/html';
  6. const CONTENT_TYPE_JSON = 'application/json';
  7. const CONTENT_TYPE_FORM_URL_ENCODED =
  8. 'application/x-www-form-urlencoded';
  9. const HEADER_CONTENT_TYPE = 'Content-Type';
  10. const TRANSPORT_HTTP = 'http';
  11. const TRANSPORT_HTTPS = 'https';
  12. const STATUS_200 = '200';
  13. const STATUS_401 = '401';
  14. const STATUS_500 = '500';
  1. 然后我们定义请求或响应所需的属性。
  1. protected $uri; // i.e. http://xxx.com/yyy
  2. protected $method; // i.e. GET, PUT, POST, DELETE
  3. protected $headers; // HTTP headers
  4. protected $cookies; // cookies
  5. protected $metaData; // information about the transmission
  6. protected $transport; // i.e. http or https
  7. protected $data = array();
  1. 顺理成章地为这些属性定义getter和setter。
  1. public function setMethod($method)
  2. {
  3. $this->method = $method;
  4. }
  5. public function getMethod()
  6. {
  7. return $this->method ?? self::METHOD_GET;
  8. }
  9. // etc.
  1. 有些属性需要通过键来访问。为此,我们定义了getXxxByKey()setXxxByKey()方法。
  1. public function setHeaderByKey($key, $value)
  2. {
  3. $this->headers[$key] = $value;
  4. }
  5. public function getHeaderByKey($key)
  6. {
  7. return $this->headers[$key] ?? NULL;
  8. }
  9. public function getDataByKey($key)
  10. {
  11. return $this->data[$key] ?? NULL;
  12. }
  13. public function getMetaDataByKey($key)
  14. {
  15. return $this->metaData[$key] ?? NULL;
  16. }
  1. 在某些情况下,请求会需要参数,我们假设参数是以PHP数组的形式存储在$data属性中。然后我们可以使用http_build_query()函数构建请求的URL。
  1. public function setUri($uri, array $params = NULL)
  2. {
  3. $this->uri = $uri;
  4. $first = TRUE;
  5. if ($params) {
  6. $this->uri .= '?' . http_build_query($params);
  7. }
  8. }
  9. public function getDataEncoded()
  10. {
  11. return http_build_query($this->getData());
  12. }
  1. 最后,我们根据原始请求设置$transport
  1. public function setTransport($transport = NULL)
  2. {
  3. if ($transport) {
  4. $this->transport = $transport;
  5. } else {
  6. if (substr($this->uri, 0, 5) == self::TRANSPORT_HTTPS) {
  7. $this->transport = self::TRANSPORT_HTTPS;
  8. } else {
  9. $this->transport = self::TRANSPORT_HTTP;
  10. }
  11. }
  12. }
  1. 在这个配方中,我们将定义一个Application\Web\Request类,当我们希望生成一个请求时,该类可以接受参数,或者,在实现一个接受请求的服务器时,用传入的请求信息填充属性。
  1. namespace Application\Web;
  2. class Request extends AbstractHttp
  3. {
  4. public function __construct(
  5. $uri = NULL, $method = NULL, array $headers = NULL,
  6. array $data = NULL, array $cookies = NULL)
  7. {
  8. if (!$headers) $this->headers = $_SERVER ?? array();
  9. else $this->headers = $headers;
  10. if (!$uri) $this->uri = $this->headers['PHP_SELF'] ?? '';
  11. else $this->uri = $uri;
  12. if (!$method) $this->method =
  13. $this->headers['REQUEST_METHOD'] ?? self::METHOD_GET;
  14. else $this->method = $method;
  15. if (!$data) $this->data = $_REQUEST ?? array();
  16. else $this->data = $data;
  17. if (!$cookies) $this->cookies = $_COOKIE ?? array();
  18. else $this->cookies = $cookies;
  19. $this->setTransport();
  20. }
  21. }
  1. 现在我们可以把注意力转移到响应类上。在这种情况下,我们将定义一个Application\Web\Received类。这个名字反映了一个事实,即我们正在重新打包从外部Web服务接收的数据。
  1. namespace Application\Web;
  2. class Received extends AbstractHttp
  3. {
  4. public function __construct(
  5. $uri = NULL, $method = NULL, array $headers = NULL,
  6. array $data = NULL, array $cookies = NULL)
  7. {
  8. $this->uri = $uri;
  9. $this->method = $method;
  10. $this->headers = $headers;
  11. $this->data = $data;
  12. $this->cookies = $cookies;
  13. $this->setTransport();
  14. }
  15. }

创建一个基于STREAMS的REST CLIENT

我们现在准备考虑两种不同的方式来实现REST客户端。第一种方法是使用底层的PHP I/O层,称为Streams。该层提供了一系列的包装器,提供对外部流资源的访问。默认情况下,任何PHP文件命令都会使用文件包装器,它提供对本地文件系统的访问。我们将使用http://https:// 包装器来实现 Application\Web\Client\Streams 适配器。

1.首先,我们定义一个Application\Web\Client\Streams类。

  1. namespace Application\Web\Client;
  2. use Application\Web\ { Request, Received };
  3. class Streams
  4. {
  5. const BYTES_TO_READ = 4096;

2.接下来,我们定义一个方法来将请求发送到外部的Web服务。在GET的情况下,我们将参数添加到URI中。在POST的情况下,我们创建一个包含元数据的流上下文,指示远程服务我们正在提供数据。使用PHP Streams,发出请求只是一个组成URI的问题,在POST的情况下,设置流上下文。然后我们使用一个简单的fopen()

  1. public static function send(Request $request)
  2. {
  3. $data = $request->getDataEncoded();
  4. $received = new Received();
  5. switch ($request->getMethod()) {
  6. case Request::METHOD_GET :
  7. if ($data) {
  8. $request->setUri($request->getUri() . '?' . $data);
  9. }
  10. $resource = fopen($request->getUri(), 'r');
  11. break;
  12. case Request::METHOD_POST :
  13. $opts = [
  14. $request->getTransport() =>
  15. [
  16. 'method' => Request::METHOD_POST,
  17. 'header' => Request::HEADER_CONTENT_TYPE
  18. . ': ' . Request::CONTENT_TYPE_FORM_URL_ENCODED,
  19. 'content' => $data
  20. ]
  21. ];
  22. $resource = fopen($request->getUri(), 'w',
  23. stream_context_create($opts));
  24. break;
  25. }
  26. return self::getResults($received, $resource);
  27. }
  1. 最后,我们来看看如何将结果检索和打包成Received对象。你会注意到,我们增加了一个规定,对以JSON格式接收的数据进行解码。
  1. protected static function getResults(Received $received, $resource)
  2. {
  3. $received->setMetaData(stream_get_meta_data($resource));
  4. $data = $received->getMetaDataByKey('wrapper_data');
  5. if (!empty($data) && is_array($data)) {
  6. foreach($data as $item) {
  7. if (preg_match('!^HTTP/\d\.\d (\d+?) .*?$!',
  8. $item, $matches)) {
  9. $received->setHeaderByKey('status', $matches[1]);
  10. } else {
  11. list($key, $value) = explode(':', $item);
  12. $received->setHeaderByKey($key, trim($value));
  13. }
  14. }
  15. }
  16. $payload = '';
  17. while (!feof($resource)) {
  18. $payload .= fread($resource, self::BYTES_TO_READ);
  19. }
  20. if ($received->getHeaderByKey(Received::HEADER_CONTENT_TYPE)) {
  21. switch (TRUE) {
  22. case stripos($received->getHeaderByKey(
  23. Received::HEADER_CONTENT_TYPE),
  24. Received::CONTENT_TYPE_JSON) !== FALSE:
  25. $received->setData(json_decode($payload));
  26. break;
  27. default :
  28. $received->setData($payload);
  29. break;
  30. }
  31. }
  32. return $received;
  33. }

定义一个基于CURL的REST客户端

现在我们来看看我们第二个REST客户端的方法,其中一个是基于cURL扩展。

1.对于这种方法,我们将假设相同的请求和响应类。初始类的定义与前面讨论的Streams客户端的定义基本相同。

  1. namespace Application\Web\Client;
  2. use Application\Web\ { Request, Received };
  3. class Curl
  4. {
  1. send()方法比使用Streams时要简单得多。我们需要做的就是定义一个选项数组,然后让cURL来完成剩下的工作。
  1. public static function send(Request $request)
  2. {
  3. $data = $request->getDataEncoded();
  4. $received = new Received();
  5. switch ($request->getMethod()) {
  6. case Request::METHOD_GET :
  7. $uri = ($data)
  8. ? $request->getUri() . '?' . $data
  9. : $request->getUri();
  10. $options = [
  11. CURLOPT_URL => $uri,
  12. CURLOPT_HEADER => 0,
  13. CURLOPT_RETURNTRANSFER => TRUE,
  14. CURLOPT_TIMEOUT => 4
  15. ];
  16. break;
  1. POST需要的cURL参数略有不同
  1. case Request::METHOD_POST :
  2. $options = [
  3. CURLOPT_POST => 1,
  4. CURLOPT_HEADER => 0,
  5. CURLOPT_URL => $request->getUri(),
  6. CURLOPT_FRESH_CONNECT => 1,
  7. CURLOPT_RETURNTRANSFER => 1,
  8. CURLOPT_FORBID_REUSE => 1,
  9. CURLOPT_TIMEOUT => 4,
  10. CURLOPT_POSTFIELDS => $data
  11. ];
  12. break;
  13. }
  1. 然后我们执行一系列的cURL函数,并通过getResults()来运行结果。
  1. $ch = curl_init();
  2. curl_setopt_array($ch, ($options));
  3. if( ! $result = curl_exec($ch))
  4. {
  5. trigger_error(curl_error($ch));
  6. }
  7. $received->setMetaData(curl_getinfo($ch));
  8. curl_close($ch);
  9. return self::getResults($received, $result);
  10. }

5.getResults()方法将结果打包成一个Received对象。

  1. protected static function getResults(Received $received, $payload)
  2. {
  3. $type = $received->getMetaDataByKey('content_type');
  4. if ($type) {
  5. switch (TRUE) {
  6. case stripos($type,
  7. Received::CONTENT_TYPE_JSON) !== FALSE):
  8. $received->setData(json_decode($payload));
  9. break;
  10. default :
  11. $received->setData($payload);
  12. break;
  13. }
  14. }
  15. return $received;
  16. }

如何运行…

请确保将前面所有的代码复制到这些类中。

  • Application\Web\AbstractHttp
  • Application\Web\Request
  • Application\Web\Received
  • Application\Web\Client\Streams
  • Application\Web\Client\Curl

在这个例子中,您可以向 Google Maps API 提出 REST 请求,以获取两点之间的驾驶方向。您还需要按照 https://developers.google.com/maps/documentation/directions/get-api-key 给出的说明,为此创建一个 API 密钥。

然后你可以定义一个chap_07_simple_rest_client_google_maps_curl.php调用脚本,使用Curl客户端发出请求。再定义一个chap_07_simple_rest_client_google_maps_streams.php调用脚本,使用Streams客户端发出请求。

  1. <?php
  2. define('DEFAULT_ORIGIN', 'New York City');
  3. define('DEFAULT_DESTINATION', 'Redondo Beach');
  4. define('DEFAULT_FORMAT', 'json');
  5. $apiKey = include __DIR__ . '/google_api_key.php';
  6. require __DIR__ . '/../Application/Autoload/Loader.php';
  7. Application\Autoload\Loader::init(__DIR__ . '/..');
  8. use Application\Web\Request;
  9. use Application\Web\Client\Curl;

你就可以得到出发地和目的地

  1. $start = $_GET['start'] ?? DEFAULT_ORIGIN;
  2. $end = $_GET['end'] ?? DEFAULT_DESTINATION;
  3. $start = strip_tags($start);
  4. $end = strip_tags($end);

现在您可以填充Request对象,并使用它来生成请求。

  1. $request = new Request(
  2. 'https://maps.googleapis.com/maps/api/directions/json',
  3. Request::METHOD_GET,
  4. NULL,
  5. ['origin' => $start, 'destination' => $end, 'key' => $apiKey],
  6. NULL
  7. );
  8. $received = Curl::send($request);
  9. $routes = $received->getData()->routes[0];
  10. include __DIR__ . '/chap_07_simple_rest_client_google_maps_template.php';

为了说明问题,你也可以定义一个模板,代表视图逻辑来显示请求的结果。

  1. <?php foreach ($routes->legs as $item) : ?>
  2. <!-- Trip Info -->
  3. <br>Distance: <?= $item->distance->text; ?>
  4. <br>Duration: <?= $item->duration->text; ?>
  5. <!-- Driving Directions -->
  6. <table>
  7. <tr>
  8. <th>Distance</th><th>Duration</th><th>Directions</th>
  9. </tr>
  10. <?php foreach ($item->steps as $step) : ?>
  11. <?php $class = ($count++ & 01) ? 'color1' : 'color2'; ?>
  12. <tr>
  13. <td class="<?= $class ?>"><?= $step->distance->text ?></td>
  14. <td class="<?= $class ?>"><?= $step->duration->text ?></td>
  15. <td class="<?= $class ?>">
  16. <?= $step->html_instructions ?></td>
  17. </tr>
  18. <?php endforeach; ?>
  19. </table>
  20. <?php endforeach; ?>

以下是浏览器中看到的请求结果。

创建一个简单的REST客户端 - 图1

更多…

PHP 标准建议(PSR-7)精确地定义了在 PHP 应用程序之间进行请求时使用的请求和响应对象。这一点在附录 “定义PSR-7类 “中做了详细的介绍。

参考

关于流的更多信息,请看这个PHP文档页http://php.net/manual/en/book.stream.php。一个经常被问到的问题是 “HTTP PUT和POST之间有什么区别?”关于这个话题的精彩讨论请参考http://stackoverflow.com/questions/107390/whats-the-difference-between-a-post-and-a-put-http-request。关于从Google获得API密钥的更多信息,请参考这些网页。

https://developers.google.com/maps/documentation/directions/get-api-key

https://developers.google.com/maps/documentation/directions/intro#Introduction