REST客户端使用超文本传输协议(HTTP)来生成对外部Web服务的请求。通过改变HTTP方法,我们可以使外部服务执行不同的操作。虽然有不少方法(或动词)可以使用,但我们将只关注GET
和POST
。本文中,我们将使用Adapter
软件设计模式来介绍实现REST客户端的两种不同方式。
如何做...
1.在我们定义REST客户端适配器之前,我们需要定义通用类来表示请求和响应信息。首先,我们将从一个抽象类开始,该类具有请求或响应所需的方法和属性。
复制 namespace Application \ Web ;
class AbstractHttp
{
2. 接下来,我们定义代表HTTP信息的类常量。
复制 const METHOD_GET = 'GET' ;
const METHOD_POST = 'POST' ;
const METHOD_PUT = 'PUT' ;
const METHOD_DELETE = ' DELETE ' ;
const CONTENT_TYPE_HTML = 'text/html' ;
const CONTENT_TYPE_JSON = 'application/json' ;
const CONTENT_TYPE_FORM_URL_ENCODED =
'application/x-www-form-urlencoded' ;
const HEADER_CONTENT_TYPE = 'Content-Type' ;
const TRANSPORT_HTTP = 'http' ;
const TRANSPORT_HTTPS = 'https' ;
const STATUS_200 = '200' ;
const STATUS_401 = '401' ;
const STATUS_500 = '500' ;
3. 然后我们定义请求或响应所需的属性。
复制 protected $uri; // i.e. http://xxx.com/yyy
protected $method; // i.e. GET, PUT, POST, DELETE
protected $headers; // HTTP headers
protected $cookies; // cookies
protected $metaData; // information about the transmission
protected $transport; // i.e. http or https
protected $data = array ();
4. 顺理成章地为这些属性定义getter和setter。
复制 public function setMethod ($method)
{
$this -> method = $method;
}
public function getMethod ()
{
return $this -> method ?? self:: METHOD_GET;
}
// etc.
5. 有些属性需要通过键来访问。为此,我们定义了getXxxByKey()
和setXxxByKey()
方法。
复制 public function setHeaderByKey ($key , $value)
{
$this -> headers[$key] = $value;
}
public function getHeaderByKey ($key)
{
return $this -> headers[$key] ?? NULL ;
}
public function getDataByKey ($key)
{
return $this -> data[$key] ?? NULL ;
}
public function getMetaDataByKey ($key)
{
return $this -> metaData[$key] ?? NULL ;
}
6. 在某些情况下,请求会需要参数,我们假设参数是以PHP数组的形式存储在$data属性中。然后我们可以使用http_build_query()
函数构建请求的URL。
复制 public function setUri ($uri , array $params = NULL )
{
$this -> uri = $uri;
$first = TRUE ;
if ($params) {
$this -> uri .= '?' . http_build_query ( $params ) ;
}
}
public function getDataEncoded ()
{
return http_build_query ( $this -> getData ()) ;
}
7. 最后,我们根据原始请求设置$transport
。
复制 public function setTransport ($transport = NULL )
{
if ($transport) {
$this -> transport = $transport;
} else {
if ( substr ( $this -> uri , 0 , 5 ) == self:: TRANSPORT_HTTPS) {
$this -> transport = self:: TRANSPORT_HTTPS;
} else {
$this -> transport = self:: TRANSPORT_HTTP;
}
}
}
8. 在这个配方中,我们将定义一个Application\Web\Request
类,当我们希望生成一个请求时,该类可以接受参数,或者,在实现一个接受请求的服务器时,用传入的请求信息填充属性。
复制 namespace Application \ Web ;
class Request extends AbstractHttp
{
public function __construct (
$uri = NULL , $method = NULL , array $headers = NULL ,
array $data = NULL , array $cookies = NULL )
{
if ( ! $headers) $this -> headers = $_SERVER ?? array ();
else $this -> headers = $headers;
if ( ! $uri) $this -> uri = $this -> headers[ 'PHP_SELF' ] ?? '' ;
else $this -> uri = $uri;
if ( ! $method) $this -> method =
$this -> headers[ 'REQUEST_METHOD' ] ?? self:: METHOD_GET;
else $this -> method = $method;
if ( ! $data) $this -> data = $_REQUEST ?? array ();
else $this -> data = $data;
if ( ! $cookies) $this -> cookies = $_COOKIE ?? array ();
else $this -> cookies = $cookies;
$this -> setTransport () ;
}
}
9. 现在我们可以把注意力转移到响应类上。在这种情况下,我们将定义一个Application\Web\Received
类。这个名字反映了一个事实,即我们正在重新打包从外部Web服务接收的数据。
复制 namespace Application \ Web ;
class Received extends AbstractHttp
{
public function __construct (
$uri = NULL , $method = NULL , array $headers = NULL ,
array $data = NULL , array $cookies = NULL )
{
$this -> uri = $uri;
$this -> method = $method;
$this -> headers = $headers;
$this -> data = $data;
$this -> cookies = $cookies;
$this -> setTransport () ;
}
}
创建一个基于STREAMS的REST CLIENT
我们现在准备考虑两种不同的方式来实现REST客户端。第一种方法是使用底层的PHP I/O层,称为Streams。该层提供了一系列的包装器,提供对外部流资源的访问。默认情况下,任何PHP文件命令都会使用文件包装器,它提供对本地文件系统的访问。我们将使用http://
或 https://
包装器来实现 Application\Web\Client\Streams
适配器。
1.首先,我们定义一个Application\Web\Client\Streams
类。
复制 namespace Application \ Web \ Client ;
use Application \ Web \ { Request , Received };
class Streams
{
const BYTES_TO_READ = 4096 ;
2.接下来,我们定义一个方法来将请求发送到外部的Web服务。在GET
的情况下,我们将参数添加到URI中。在POST
的情况下,我们创建一个包含元数据的流上下文,指示远程服务我们正在提供数据。使用PHP Streams,发出请求只是一个组成URI的问题,在POST
的情况下,设置流上下文。然后我们使用一个简单的fopen()
。
复制 public static function send ( Request $request)
{
$data = $request -> getDataEncoded () ;
$received = new Received ();
switch ($request -> getMethod () ) {
case Request :: METHOD_GET :
if ($data) {
$request -> setUri ( $request -> getUri () . '?' . $data ) ;
}
$resource = fopen ( $request -> getUri (), 'r' ) ;
break ;
case Request :: METHOD_POST :
$opts = [
$request -> getTransport () =>
[
'method' => Request :: METHOD_POST ,
'header' => Request :: HEADER_CONTENT_TYPE
. ': ' . Request :: CONTENT_TYPE_FORM_URL_ENCODED ,
'content' => $data
]
];
$resource = fopen ( $request -> getUri (), 'w' ,
stream_context_create ( $opts )) ;
break ;
}
return self:: getResults ( $received , $resource ) ;
}
3. 最后,我们来看看如何将结果检索和打包成Received
对象。你会注意到,我们增加了一个规定,对以JSON格式接收的数据进行解码。
复制 protected static function getResults ( Received $received , $resource)
{
$received -> setMetaData ( stream_get_meta_data ( $resource )) ;
$data = $received -> getMetaDataByKey ( 'wrapper_data' ) ;
if ( ! empty ( $data ) && is_array ( $data ) ) {
foreach ($data as $item) {
if ( preg_match ( '!^HTTP/\d\.\d (\d+?) .*?$!' ,
$item , $matches ) ) {
$received -> setHeaderByKey ( 'status' , $matches[ 1 ] ) ;
} else {
list ( $key , $value ) = explode ( ':' , $item ) ;
$received -> setHeaderByKey ( $key , trim ( $value )) ;
}
}
}
$payload = '' ;
while ( ! feof ( $resource ) ) {
$payload .= fread ( $resource , self:: BYTES_TO_READ ) ;
}
if ($received -> getHeaderByKey ( Received :: HEADER_CONTENT_TYPE ) ) {
switch ( TRUE ) {
case stripos ( $received -> getHeaderByKey (
Received :: HEADER_CONTENT_TYPE ),
Received :: CONTENT_TYPE_JSON ) !== FALSE :
$received -> setData ( json_decode ( $payload )) ;
break ;
default :
$received -> setData ( $payload ) ;
break ;
}
}
return $received;
}
定义一个基于CURL的REST客户端
现在我们来看看我们第二个REST客户端的方法,其中一个是基于cURL扩展。
1.对于这种方法,我们将假设相同的请求和响应类。初始类的定义与前面讨论的Streams客户端的定义基本相同。
复制 namespace Application \ Web \ Client ;
use Application \ Web \ { Request , Received };
class Curl
{
2. send()
方法比使用Streams
时要简单得多。我们需要做的就是定义一个选项数组,然后让cURL
来完成剩下的工作。
复制 public static function send ( Request $request)
{
$data = $request -> getDataEncoded () ;
$received = new Received ();
switch ($request -> getMethod () ) {
case Request :: METHOD_GET :
$uri = ($data)
? $request -> getUri () . '?' . $data
: $request -> getUri () ;
$options = [
CURLOPT_URL => $uri ,
CURLOPT_HEADER => 0 ,
CURLOPT_RETURNTRANSFER => TRUE ,
CURLOPT_TIMEOUT => 4
];
break ;
3. POST
需要的cURL
参数略有不同
复制 case Request :: METHOD_POST :
$options = [
CURLOPT_POST => 1 ,
CURLOPT_HEADER => 0 ,
CURLOPT_URL => $request -> getUri (),
CURLOPT_FRESH_CONNECT => 1 ,
CURLOPT_RETURNTRANSFER => 1 ,
CURLOPT_FORBID_REUSE => 1 ,
CURLOPT_TIMEOUT => 4 ,
CURLOPT_POSTFIELDS => $data
];
break ;
}
4. 然后我们执行一系列的cURL函数,并通过getResults()
来运行结果。
复制 $ch = curl_init () ;
curl_setopt_array ( $ch , ($options) ) ;
if ( ! $result = curl_exec ( $ch ) )
{
trigger_error ( curl_error ( $ch )) ;
}
$received -> setMetaData ( curl_getinfo ( $ch )) ;
curl_close ( $ch ) ;
return self:: getResults ( $received , $result ) ;
}
5.getResults()
方法将结果打包成一个Received
对象。
复制 protected static function getResults ( Received $received , $payload)
{
$type = $received -> getMetaDataByKey ( 'content_type' ) ;
if ($type) {
switch ( TRUE ) {
case stripos ( $type ,
Received :: CONTENT_TYPE_JSON ) !== FALSE ):
$received -> setData ( json_decode ( $payload )) ;
break ;
default :
$received -> setData ( $payload ) ;
break ;
}
}
return $received;
}
如何运行...
请确保将前面所有的代码复制到这些类中。
Application\Web\AbstractHttp
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
客户端发出请求。
复制 <? php
define ( 'DEFAULT_ORIGIN' , 'New York City' ) ;
define ( 'DEFAULT_DESTINATION' , 'Redondo Beach' ) ;
define ( 'DEFAULT_FORMAT' , 'json' ) ;
$apiKey = include __DIR__ . '/google_api_key.php' ;
require __DIR__ . '/../Application/Autoload/Loader.php' ;
Application \ Autoload \ Loader :: init ( __DIR__ . '/..' ) ;
use Application \ Web \ Request ;
use Application \ Web \ Client \ Curl ;
你就可以得到出发地和目的地
复制 $start = $_GET[ 'start' ] ?? DEFAULT_ORIGIN;
$end = $_GET[ 'end' ] ?? DEFAULT_DESTINATION;
$start = strip_tags ( $start ) ;
$end = strip_tags ( $end ) ;
现在您可以填充Request对象,并使用它来生成请求。
复制 $request = new Request (
'https://maps.googleapis.com/maps/api/directions/json' ,
Request :: METHOD_GET ,
NULL ,
[ 'origin' => $start , 'destination' => $end , 'key' => $apiKey] ,
NULL
);
$received = Curl :: send ( $request ) ;
$routes = $received -> getData () -> routes[ 0 ];
include __DIR__ . '/chap_07_simple_rest_client_google_maps_template.php' ;
为了说明问题,你也可以定义一个模板,代表视图逻辑来显示请求的结果。
复制 <? php foreach ($routes -> legs as $item) : ?>
<!-- Trip Info -->
< br > Distance : <?= $item -> distance -> text; ?>
< br > Duration : <?= $item -> duration -> text; ?>
<!-- Driving Directions -->
< table >
< tr >
< th > Distance </ th >< th > Duration </ th >< th > Directions </ th >
</ tr >
<? php foreach ($item -> steps as $step) : ?>
<? php $class = ($count ++ & 01 ) ? 'color1' : 'color2' ; ?>
< tr >
< td class= "<?= $class ?>" ><?= $step -> distance -> text ?></ td >
< td class= "<?= $class ?>" ><?= $step -> duration -> text ?></ td >
< td class= "<?= $class ?>" >
<?= $step -> html_instructions ?></ td >
</ tr >
<? php endforeach ; ?>
</ table >
<? php endforeach ; ?>
以下是浏览器中看到的请求结果。
更多...
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