REST clients use HyperText Transfer Protocol (HTTP) to generate requests to external web services. By changing the HTTP method, we can cause the external service to perform different operations. Although there are quite a few methods (or verbs) available, we will only focus on GET and POST. In this recipe, we will use the Adapter software design pattern to present two different ways of implementing a REST client.
namespace Application\Web;
class AbstractHttp
{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';
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();
public function setMethod($method)
{
$this->method = $method;
}
public function getMethod()
{
return $this->method ?? self::METHOD_GET;
}
// etc.getXxxByKey() and setXxxByKey() methods: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;
}$data property. We can then build the request URL using the http_build_query() function: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());
}$transport based on the original request: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;
}
}
}Application\Web\Request class that can accept parameters when we wish to generate a request, or, alternatively, populate properties with incoming request information when implementing a server that accepts requests: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();
}
}Application\Web\Received class. The name reflects the fact that we are re-packaging data received from the external web service: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();
}
}We are now ready to consider two different ways to implement a REST client. The first approach is to use an underlying PHP I/O layer referred to as Streams. This layer provides a series of wrappers that provide access to external streaming resources. By default, any of the PHP file commands will use the file wrapper, which gives access to the local filesystem. We will use the http:// or https:// wrappers to implement the Application\Web\Client\Streams adapter:
Application\Web\Client\Streams class:namespace Application\Web\Client;
use Application\Web\ { Request, Received };
class Streams
{
const BYTES_TO_READ = 4096;GET, we add the parameters to the URI. In the case of POST, we create a stream context that contains metadata instructing the remote service that we are supplying data. Using PHP Streams, making a request is just a matter of composing the URI, and, in the case of POST, setting the stream context. We then use a simple 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);
}Received object. You will notice that we added a provision to decode data received in JSON format: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;
}We will now have a look at our second approach for a REST client, one of which is based on the cURL extension:
namespace Application\Web\Client;
use Application\Web\ { Request, Received };
class Curl
{send() method is quite a bit simpler than when using Streams. All we need to do is to define an array of options, and let cURL do the rest: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;POST requires slightly different cURL parameters: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;
}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);
}getResults() method packages results into a Received object: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;
}Be sure to copy all the preceding code into these classes:
Application\Web\AbstractHttpApplication\Web\RequestApplication\Web\ReceivedApplication\Web\Client\StreamsApplication\Web\Client\CurlFor this illustration, you can make a REST request to the Google Maps API to obtain driving directions between two points. You also need to create an API key for this purpose by following the directions given at https://developers.google.com/maps/documentation/directions/get-api-key.
You can then define a chap_07_simple_rest_client_google_maps_curl.php calling script that issues a request using the Curl client. You might also consider define a chap_07_simple_rest_client_google_maps_streams.php calling script that issues a request using the Streams client:
<?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;You can then get the origin and destination:
$start = $_GET['start'] ?? DEFAULT_ORIGIN; $end = $_GET['end'] ?? DEFAULT_DESTINATION; $start = strip_tags($start); $end = strip_tags($end);
You are now in a position to populate the Request object, and use it to generate the 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';
For the purposes of illustration, you could also define a template that represents view logic to display the results of the request:
<?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; ?>Here are the results of the request as seen in a browser:

PHP Standards Recommendations (PSR-7) precisely defines request and response objects to be used when making requests between PHP applications. This is covered extensively in Appendix, Defining PSR-7 Classes .
For more information on
Streams, see this PHP documentation page http://php.net/manual/en/book.stream.php. An often asked question is "what is the difference between HTTP PUT and POST?" for an excellent discussion on this topic please refer to http://stackoverflow.com/questions/107390/whats-the-difference-between-a-post-and-a-put-http-request. For more information on obtaining an API key from Google, please refer to these web pages:
https://developers.google.com/maps/documentation/directions/get-api-key
https://developers.google.com/maps/documentation/directions/intro#Introduction