Guzzling web services out of the box…

Avatar von Andreas Haberberger

Guzzle has come to a certain fame recently as the framework powering Amazon Web Services new SDK2 for PHP. But what exactly is Guzzle? Guzzle is a framework for building RESTful web service clients. As such it is obviously a very potent HTTP-client library, that presents us with an easy way to send subtly crafted HTTP requests of all flavours to a server and deal with the resulting responses.

But Guzzle is still more than that. Based on a service description file written in JSON – similar to a WSDL-file from the SOAP world – Guzzle builds entire web service clients from scratch and as such relieves us from tedious and repetitive tasks in their implementation.
Let’s have a look at an example: I prepared a very, very basic web service in django/tastypie (The python-framework being the fastest means – to my knowledge – for getting a RESTful Webservice up and running), that can be cloned from github and that consists of only two simple ressources: A „product“ resource (http://localhost:8000/api/v1/product) and a „price“ resource (http://localhost:8000/api/v1/price). Both are connected through a 1:n relation so that one „product“ can have any number of „prices“. Django/tastypie provides us with support for the usual RESTful HTTP-verbs. Very basic, isn’t it? But I’ve told you so. Now let’s procede to the interesting part (at least I hope so…)

Installation

Guzzle can be installed in various ways. You can either fetch it from PEAR or install it over the nowadays so popular Composer dependency managenment. Prospective contributors can also choose to directly clone/fork Guzzle’s public github repository.

Unless you choose the developer version from github, you end up with a neatly prepared project directory containing a Guzzle-plus-dependencies „vendor“ folder and a lot of space for our application.

A simple Composer-typical

require_once 'vendor/autoload.php';

in any source code file is enough to get it all set up. I decided to make good use of Composer’s autoload function and add my own MyApp-Namespace to it in my composer.json file like this:

{
    "require": {
        "guzzle/guzzle": "3.0.*"
    },
    "autoload": {
        "psr-0": {"MyApp": "src/"}
    }
}

The entire source code of the client application can be cloned from here and is organised in the following fashion:

|-- client.php
|-- src
|    |-- MyApp
|    |    |-- App.php
|    |
|    |-- Product
|         |-- services.json
|
|-- vendor

Service description

First we create most of Guzzle’s magic in our service description. This is the central place where interaction with our target webservice is defined.

{
    "name": "Product",
    "apiVersion": "2012-11-25",
    "baseUrl": "http://localhost:8000",
    "description": "Product API",
    "operations": {
        "GetProducts": {
            "httpMethod": "GET",
            "uri": "/api/v1/product/",
            "summary": "Gets a list of Products",
            "responseClass": "GetProductsOutput"
        },
        "GetProduct": {
            "httpMethod": "GET",
            "uri": "/api/v1/product/{id}",
            "summary": "Retrieves a single Product",
            "responseClass": "GetProductOutput",
            "parameters": {
                "id": {
                    "location": "uri",
                    "description": "Product to retrieve by ID",
                    "required": true
                }
            }
        },
        "CreateProduct": {
            "httpMethod": "POST",
            "uri": "/api/v1/product/",
            "summary": "Creates a new Product",
            "responseClass": "CreateProductOutput",
            "parameters": {
                "name": {
                    "location": "json",
                    "type": "string"
                },
                "stock": {
                    "location": "json",
                    "type": "integer"
                },
                "prices": {
                    "location": "json",
                    "type": "array"
                }
            }
        },
        "GetPrices": {
            "httpMethod": "GET",
            "uri": "/api/v1/price/",
            "summary": "Gets a list of Prices",
            "responseClass": "GetPricesOutput"
        },
        "GetPrice": {
            "httpMethod": "GET",
            "uri": "/api/v1/price/{id}",
            "summary": "Retrieves a single Product",
            "responseClass": "GetPriceOutput",
            "parameters": {
                "id": {
                    "location": "uri",
                    "description": "Price to retrieve by ID",
                    "required": true
                }
            }
        },
        "CreatePrice": {
            "httpMethod": "POST",
            "uri": "/api/v1/price/",
            "summary": "Creates a new Price",
            "responseClass": "CreatePriceOutput",
            "parameters": {
                "name": {
                    "location": "json",
                    "type": "string"
                },
                "amount": {
                    "location": "json",
                    "type": "integer"
                },
                "product": {
                    "location": "json",
                    "type": "string"
                }
            }
        }
  	},
    "models": {
        "Product": {
            "type": "object",
            "properties": {
                "id": {
                    "location": "json",
                    "type": "integer"
                },
                "name": {
                    "location": "json",
                    "type": "string"
                },
                "prices": {
                    "location": "json",
                    "type": "array",
                    "items": {
                    	"location": "json",
                    	"type": "string"
                    }
                },
                "resource_uri": {
                    "location": "json",
                    "type": "string"
                },
                "stock": {
                    "location": "json",
                    "type": "integer"
                }
            }
        },
        "Price": {
            "type": "object",
            "properties": {
                "id": {
                    "location": "json",
                    "type": "integer"
                },
                "name": {
                    "location": "json",
                    "type": "string"
                },
                "product": {
                    "location": "json",
                    "type": "string"
                },
                "resource_uri": {
                    "location": "json",
                    "type": "string"
                },
                "amount": {
                    "location": "json",
                    "type": "number"
                }
            }
        },
        "GetProductOutput": {
        	"$ref": "Product"
        },
    	"GetProductsOutput": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
		        "limit" : {
			    "location": "json",
			    "type": "integer"
			},
			"next" : {
			    "location": "json",
			    "type": "string"
			},
			"offset" : {
			    "location": "json",
			    "type": "integer"
			},
			"previous" : {
			    "location": "json",
			    "type": "string"
			},
			"total_count" : {
			    "location": "json",
			    "type": "integer"
			}
                    }
                },
                "objects": {
                    "type": "array",
                    "items": {
                        "$ref": "Product"
                    }
                }
            }
        },
        "GetPriceOutput": {
        	"$ref": "Price"
        },
    	"GetPricesOutput": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
		        "limit" : {
			    "location": "json",
			    "type": "integer"
			},
			"next" : {
			    "location": "json",
			    "type": "string"
			},
			"offset" : {
			    "location": "json",
			    "type": "integer"
			},
			"previous" : {
			    "location": "json",
			    "type": "string"
			},
			"total_count" : {
			    "location": "json",
			    "type": "integer"
			}
                    }
                },
                "objects": {
                    "type": "array",
                    "items": {
                        "$ref": "Price"
                    }
                }
            }
        },
        "CreateProductOutput": {
            "type": "object",
            "properties": {
                "location": {
                    "location": "header",
                    "sentAs": "Location",
                    "type": "string"
                },
                "uri": {
                    "location": "json",
                    "sentAs": "resource_uri",
                    "type": "string"
                }
            }
        },
        "CreatePriceOutput": {
            "type": "object",
            "properties": {
                "location": {
                    "location": "header",
                    "sentAs": "Location",
                    "type": "string"
                },
                "uri": {
                    "location": "json",
                    "sentAs": "resource_uri",
                    "type": "string"
                }
            }
        }
    }
}

Our service description consists of two main sections framed by some meta information. The first section contains definitions of „operations“. These correspond mostly with the various sorts of requests we wish to send to our RESTful service and consist of fields stating the HTTP method used or the requests URI which are quite self-explaining, I think. Remarkable is the presence of a field called „responseClass“ which references a model the web service’s response is going to mapped on. More on this in an instant. The other remarkable feature of the „operations“ definition is the „parameters“ section. Not very surprisingly, this defines parameters to be sent in our web service request. The location of each parameter – be it (in our case) „uri“ for parameters to be appeded to the URI or „json“ for those included in POST request bodies.

The second section defines the „responseClass“ models mentioned above. These can be exact images of single resources as for example in the „Product“ model, of full response data sent by our web service, as in the „GetProductsResult“ model, where additional pagination meta data is provided or a stripped down set of result data as in the „CreateProductResult“ model where we are only interested in the newly created resource’s URI. Here we have to be a little cunning. As of RFC 2616 a „Location“ header of an HTTP response is required to be an absolute URL. This is not exactly what Guzzle likes to guzzle as a pointer to our related „product“ resource, it rather demands to be fed a relative path. Instead of doing some PHP string manipulation magic, I decided to „persuade“ our django/tastypie web service to deliver created resources in its POST-response’s body and extract the relevant „resource_uri“ parameter in our response class. Thus we are within HTTP specs as well as the REST pattern. Wheew…

In general, information for our result model can be taken from various sources like – obviously – the body of the response or its headers as in the „Create…“ examples.

The App

Next step is an Application class under our MyApp-namespace that is by now generously served by Composer.

<?php
namespace MyApp;

use Guzzle\Service\Client;
use Guzzle\Service\Description\ServiceDescription;

class App {

	/* @var $_client Guzzle\Service\Client */
	protected $_client;

	/**
	 * Construct the app
	 */
	function __construct()
	{
		$this->_client = new Client();
		$this->_client->setDescription(
				ServiceDescription::factory('src/Product/services.json')
		);
	}

	/**
	 * Retrieve a list of product resources (simplified) via GET Request
	 * @return \MyApp\Guzzle\Http\Message\Response
	 */
	public function getProductList()
	{
		/* @var $command Guzzle\Service\Command\AbstractCommand */
		$command = $this->_client->getCommand('GetProducts');
		/* @var $response Guzzle\Http\Message\Response */
		$response = $this->_client->execute($command);
		return $response;
	}

	/**
	 * Retieve a single product resource by ID via GET Request
	 * @param integer $_id
	 * @return \MyApp\Guzzle\Http\Message\Response
	 */
	public function getProduct($_id)
	{
		/* @var $command Guzzle\Service\Command\AbstractCommand */
		$command = $this->_client->getCommand('GetProduct', array('id' => $_id));
		/* @var $response Guzzle\Http\Message\Response */
		$response = $this->_client->execute($command);
		return $response;
	}

	/**
	 * Create a product resource via POST Request
	 * @param string $name
	 * @param integer $stock
	 * @param array $prices
	 * @return \MyApp\Guzzle\Http\Message\Response
	 */
	public function createProduct($name, $stock, array $prices = array())
	{
		/* @var $command Guzzle\Service\Command\AbstractCommand */
		$command = $this->_client->getCommand(
				'CreateProduct', 
				array('name' => $name, 'stock' => $stock, 'prices' => $prices)
		);
		$command->set("command.headers", array("Content-type" => "application/json"));
		/* @var $response Guzzle\Http\Message\Response */
		$response = $this->_client->execute($command);
		return $response;
	}

	/**
	 * Retrieve a list of price resources (simplified) via GET Request
	 * @return \MyApp\Guzzle\Http\Message\Response
	 */
	public function getPriceList()
	{
		/* @var $command Guzzle\Service\Command\AbstractCommand */
		$command = $this->_client->getCommand('GetPrices');
		/* @var $response Guzzle\Http\Message\Response */
		$response = $this->_client->execute($command);
		return $response;
	}

	/**
	 * Retieve a single price resource by ID via GET Request
	 * @param integer $_id
	 * @return \MyApp\Guzzle\Http\Message\Response
	 */
	public function getPrice($_id)
	{
		/* @var $command Guzzle\Service\Command\AbstractCommand */
		$command = $this->_client->getCommand('GetPrice', array('id' => $_id));
		/* @var $response Guzzle\Http\Message\Response */
		$response = $this->_client->execute($command);
		return $response;
	}

	/**
	 * Create a price resource via POST Request
	 * @param string $name
	 * @param double $amount
	 * @param string $product
	 * @return \MyApp\Guzzle\Http\Message\Response
	 */
	public function createPrice($name, $amount, $product)
	{
		/* @var $command Guzzle\Service\Command\AbstractCommand */
		$command = $this->_client->getCommand(
				'CreatePrice', 
				array('name' => $name, 'amount' => $amount, 'product' => $product)
		);
		$command->set("command.headers", array("Content-type" => "application/json"));
		/* @var $response Guzzle\Http\Message\Response */
		$response = $this->_client->execute($command);
		return $response;
	}
}

This application basically contains the construction code for our webservice client, that Guzzle concocts all alone from the instructions in our service description.

Further we add some methods, that bring the operations from our service description to life. Those are quite straightforard besides one small potential pitfall. Guzzle by default uses a „Content-type“ header of „application/x-www-form-urlencoded“ which is quite helpful in emulating HTML-form output but in our case is simply incorrect. So we persuade our client to use „application/json“ in its request which is what actually happens and which satisfies the web service perfectly.

From our API-command we receive an instance of Guzzle\Service\Resource\Model that has data content crafted after the relevant model section in our service description.

Consumption begins…

So everything is set for consumption of our web service. The following example shows how our App-class is used to create and retrieve resources from our web service. Enjoy…

<?php
require_once 'vendor/autoload.php';

$app = new \MyApp\App();

// Create a single product

$product = $app->createProduct("First Product", 120);

// Create a price for our first product

$app->createPrice("First Price", 12, $product['uri']);

// Create a product with two prices

$product = $app->createProduct(
		"Second Product",
		150,
		array(
			array("name" => "New Price", "amount" => 10),
			array("name" => "Another new Price", "amount" => 11),
			)
		);

// Retrieve all products

$products = $app->getProductList();

This was only a very simple example for building web service clients on Guzzle. But the framework holds more in store for us, like a full blown event model that lets us react to all kinds of contingencies or the possibility to receive POPOs (Plain Old PHP Objects) instead of Guzzle’s own Model instances… Lots to be explored, stay tuned!

Avatar von Andreas Haberberger

Kommentare

Eine Antwort zu „Guzzling web services out of the box…“

  1. Lesenswert: Guzzling web services out of the box… http://t.co/kBydJ7j9

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.