Monday, November 11, 2013

PHP REST client with Pest

Introduction

In this post I'm going to show a very nice and simple way of creating PHP clients to RESTful web services. For this we are going to use a library called Pest, available on GitHub.

Prerequisities

Pest requires the mod_curl extension, so please make sure it is properly installed on your web server before proceeding further.

Project structure

The next step is to download the library files and put them in a separate folder called /Pest. We will also create an example index.php script, where we will put several code snippets calling a REST service. Here is the final project structure:

|
+- Pest/
|  |
|  +- Pest.php
|  +- PestJSON.php
|  +- PestXML.php
|
+- index.php

Include Pest

At the beginning of the index.php lets include the Pest library:

<?php

require_once 'Pest/Pest.php';
require_once 'Pest/PestJSON.php';
require_once 'Pest/PestXML.php';

Plain HTTP

We will start with a simple example of calling our service using plain HTTP GET method:

$address = "http://127.0.0.1:8080";
$pest = new Pest($address);
$result = $pest->get("/my/rest/uri");

In this example the $result variable contains a raw string, so data processing must be manually done by the programmer. It means that you will have to implement unmarshalling logic, for example from JSON, XML, YAML, etc., all by yourself.

Now, lets see how we can POST some data and headers:

$data = array(
 "i" => 123,
 "str" => "Bob",
 "arr" => array("a", "b", "c"),
 "map" => array(
  "a" => 1,
  "b" => 2,
  "c" => 3
 )
);

$headers = array(
 "abc" => "123"
);

$result = $pest->post("/my/rest/uri", $data, $headers);

In the service data is available through the parameters map:

i -> 123
str -> Bob
arr[0] -> a
arr[1] -> b
arr[2] -> c
map[a] -> 1
map[b] -> 2
map[c] -> 3

...and headers are available through the headers map:

abc -> 123

Again, we have to manually process the $result. This is a tedious task and a programmer shouldn't be involved in the most cases. In the following examples we will look into ways of automatically marshalling/unmarshalling data to/from JSON/XML.

JSON

Supposedly, we have a REST service accessible by calling GET method on the uri /my/rest/uri.json returning this data:

{ 
  "i": 42, 
  "str": "Alice", 
  "arr": ["d", "e", "f"], 
  "map": { "x": 123, "y": 456 } 
}

We can "turn on" the automatic unmarshalling from JSON to PHP objects tree by replacing the Pest object with PestJSON:

$address = "http://127.0.0.1:8080";
$pest = new PestJSON($address);
$result = $pest->get("/my/rest/uri.json");
print_r($result);

The upper script prints out:

Array ( 
  [i] => 42 
  [str] => Alice 
  [arr] => Array ( [0] => d [1] => e [2] => f ) 
  [map] => Array ( [x] => 123 [y] => 456 ) 
)

With switching to the PestJSON we also get automatic marshalling to JSON. If we POST this data to our REST service:

$data = array(
 "i" => 123,
 "str" => "Bob",
 "arr" => array("a", "b", "c"),
 "map" => array(
  "a" => 1,
  "b" => 2,
  "c" => 3
 )
);
$result = $pest->post("/my/rest/uri.json", $data);

...on the server in the body we receive:

{"i":123,"str":"Bob","arr":["a","b","c"],"map":{"a":1,"b":2,"c":3}}

XML

A similar case is with the XML format. Lets create a REST service accessible by calling GET on /my/rest/uri.xml. The service returns this XML:

<grandparent>
  <parent>
    <child>
      <i>123</i>
      <str>Frank</str>
      <list>
        <item>abc</item>
        <item>def</item>
        <item>ghi</item>
      </list>
      <map>
        <item key="a">1</item>
        <item key="b">2</item>
        <item key="c">3</item>
      </map>
    </child>
  </parent>
</grandparent>

Again, lets call it from Pest by replacing the Pest class with PestXML subclass:

$address = "http://127.0.0.1:8080";
$pest = new PestXML($address);
$result = $pest->get("/my/rest/uri.xml");
print_r($result);

The script echoes the following PHP object tree:

SimpleXMLElement Object ( 
  [parent] => SimpleXMLElement Object ( 
    [child] => SimpleXMLElement Object ( 
      [i] => 123 
      [str] => Frank 
      [list] => SimpleXMLElement Object ( 
        [item] => Array ( 
          [0] => abc 
          [1] => def 
          [2] => ghi 
        ) 
      ) 
      [map] => SimpleXMLElement Object ( 
        [item] => Array ( 
          [0] => 1 
          [1] => 2 
          [2] => 3 
        ) 
      ) 
    ) 
  ) 
)

To access response we can either traverse the object tree in the standard PHP way:

$i = (int) $result->parent->child->i;

...or by using XPath:

$queryResult = $result->xpath("/grandparent/parent/child/i");
$i = (int) $queryResult[0];

The first example looks simpler, however XPath allows us to execute more advanced queries. What's more, we don't have to test the nullity of objects - the query will just return empty results array. In the first example, if parent or child was absent, an exception is thrown.

Lets take a closer look at a case where XPath seems more suitable. The following code extracts the item with key equal to "a":

$queryResult = $result->xpath("/grandparent/parent/child/map/item[@key='a']");
$item = $queryResult[0];
$key = (string) $item['key'];
$value = (int) $item;
echo "$key -> $value";

The result:

a -> 1

Lets finish with a little gotcha (which may be fixed in the future). If we POST this data:

$data = array(
 "i" => 123,
 "str" => "Bob",
 "arr" => array("a", "b", "c"),
 "map" => array(
  "a" => 1,
  "b" => 2,
  "c" => 3
 )
);

$result = $pest->post("/my/rest/uri.xml", $data);

...on the server side the request will have empty body and data is passed like in plain HTTP (through parameters map):

i -> 123
str -> Bob
arr[0] -> a
arr[1] -> b
arr[2] -> c
map[a] -> 1
map[b] -> 2
map[c] -> 3

The End

I hope you enjoyed this post and as me you found the Pest library to be simple and robust.

1 comment:

  1. Hi Matthew, thank you, very nice example. Easy to follow and duplicate.

    ReplyDelete