My First Tonic App
Roy Fielding's dissertation has the following to say about resources:
The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource. A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.
In Tonic, resources are modelled as a PHP class. This class holds a collection of data and methods to manipulate that data including those that are tied to our incoming HTTP requests methods (GET, POST, etc).
If you haven't seen the quick example Hello World application, go have a read first.
You can get the source files for this tutorial in this archive.Writing your first resource
So lets look at a real world resource example, lets say we want to create a resource that reports the current weather in London. Presuming we have a way of getting that information, we might write a resource that looks something like this:
<?php
class Weather extends SmartyResource {
function get(&$request) {
$weatherService =& new weatherService();
$weatherReport = $weatherService->getWeather('london');
$this->_smarty->assign('weatherReport', $weatherReport);
return parent::get($request); // respond to HTTP request
}
}
?>
lib/weather.php
And then we might create a representation for this resource that looks something like this:
class: weather
mimetype: application/weather+xml
<?xml version="1.0"?>
<weather location="{$weatherReport.location|escape}">
<summary>{$weatherReport.summary|escape}</summary>
<temperature>{$weatherReport.temperature|escape}</temperature>
<wind>{$weatherReport.wind|escape}</wind>
<humidity>{$weatherReport.humidity|escape}</humidity>
<pressure>{$weatherReport.pressure|escape}</pressure>
<visiblity>{$weatherReport.visiblity|escape}</visiblity>
</weather>
resources/weather.xml
So now if we hit /weather.xml we'll get our weather report.
Using data adapters
All we've really done above is use our resource class like a controller talking to our data source via our own API, but we can go one better and connect the data directly into Tonic so that our data becomes resources within the system.
Tonic connects to data via persistance adapters, a class that wraps up access to the data exposing it as resources. It comes with 2 persistance adapters out of the box, a file system adapter that connects to files on the file system and is the adapter we used by default in the code above, and a SQL adapter that connects to SQL databases.
To hook up to our weather data, we create a new adapter that talks to our weather data source:
<?php
class WeatherAdapter extends Adapter {
var $weatherService;
function weatherAdapter(&$mimetypes) {
$this->weatherService =& new weatherService();
parent::adapter($mimetypes);
}
function _getLocationFromUrl($url) {
$urlParts = explode('/', $url);
return array_pop($urlParts);
}
function &select($url, $options = array()) {
if ($data = $this->weatherService->getWeather($this->_getLocationFromUrl($url))) {
$data['url'] = $url; // don't forget to include the URL as part of the data
return array($url => $data);
} else {
return array();
}
}
function insert(&$resource) {
$data = array();
if (isset($resource->summary)) $data['summary'] = $resource->summary;
if (isset($resource->temperature)) $data['temperature'] = $resource->temperature;
if (isset($resource->wind)) $data['wind'] = $resource->wind;
if (isset($resource->humidity)) $data['humidity'] = $resource->humidity;
if (isset($resource->pressure)) $data['pressure'] = $resource->pressure;
if (isset($resource->visiblity)) $data['visiblity'] = $resource->visiblity;
return $this->weatherService->setWeather($this->_getLocationFromUrl($resource->url), $data);
}
function update(&$resource) {
return $this->insert($resource);
}
function delete($url) {
return $this->weatherService->deleteWeather($this->_getLocationFromUrl($url));
}
}
?>
adapter/weatherAdapter.php
If we now adjust our dispatcher to use our new adapter then we can start accessing our weather data:
$adapter =& new WeatherAdapter($mimetypes);
// create a request object based upon the incoming HTTP request
$request =& new Request();
// load resource
$resource =& $request->load($adapter);
// load the representation resource
if ($resource && $representation =& $resource->loadRepresentation($adapter)) {
$response =& $representation->get($request);
}
// output the response doing encoding as required
$response->output();
dispatch.php
Now we have exposed all of our weather data. If you visit /london
you'll get a raw output of the data since we haven't hooked up a custom representation
for it.
summary: cloudy 7°C temperature: 7°C wind: Southerly 8 mph humidity: 79% pressure: 1009mB visiblity: Very good location: london url: /london class: resource mimetype: application/tonic-resource created: 1200252381 modified: 1200252381/london
So lets attach our XML representation to it:
class: SmartyResource
mimetype: application/weather+xml
<?xml version="1.0"?>
<weather location="{$resource->location|escape}">
<summary>{$resource->summary|escape}</summary>
<temperature>{$resource->temperature|escape}</temperature>
<wind>{$resource->wind|escape}</wind>
<humidity>{$resource->humidity|escape}</humidity>
<pressure>{$resource->pressure|escape}</pressure>
<visiblity>{$resource->visiblity|escape}</visiblity>
</weather>
resources/weather.xml
Note that our representation no longer requires our Weather resource class since it now does nothing beyond display the data already fetched by the data adapter. We have also changed the Smarty references to retrieve the values from the parent resources properties rather than from a variable assigned into the representations resource.
$adapter =& new FileAdapter($mimetypes, 'resources');
$weatherAdapter =& new WeatherAdapter($mimetypes);
// create a request object based upon the incoming HTTP request
$request =& new Request();
// load resource forcing a particular representation
$resource =& $request->load($weatherAdapter, array(
TONIC_FIND_FORCE_METADATA => array(
'representation' => '/weather.xml'
)
));
// load the representation resource
if ($resource && $representation =& $resource->loadRepresentation($adapter)) {
$response =& $representation->get($request);
}
// output the response doing encoding as required
$response->output();
dispatch.php
We'll need both our weather adapter and the file system adapter since we need
to load the resource containing the representation from the file system. We also
need to tell the Request::load method that we want the resources returned
to use our representation by forcing the representation URL into the resources
"representation" property.
So now, once our resource is loaded, the final part of our dispatcher will see that we have an acceptable representation to load and load it.
Updating the weather
So far we've created a read only weather service, but how does the weather information get in there in the first place? Well, why not also via our weather service. All we need to do is add the ability to update our weather resources.
<?php
class Weather extends Resource {
function &put(&$request, &$adapter) {
return $this->_updateResource($request, $adapter);
}
}
?>
lib/weather.php
Here we're taking the data passed in via the HTTP PUT request and saving it by using the standard resource update method and our data adapter takes care of the actual saving.
Of course we also need to parse the input data into a format we can deal with, luckily Tonic provides us a convenient hook. When we use our Request object to load our resource, it reads any request data that was sent in with the request and tries to convert it into an array that can be used to update the resource.
If we add a new private method to our Request object that has a name that corrisponds to the mimetype of the input format, it will get called to do this conversion when we load our resource.
<?php
class WeatherRequest extends Request {
function &_parseFormatApplicationWeatherXml() {
$xml = xmlToArray($this->body);
$data = array();
if (isset($xml['weather']['@location'])) $data['location'] = $xml['weather']['@location'];
if (isset($xml['weather']['summary']['_text'])) $data['summary'] = $xml['weather']['summary']['_text'];
if (isset($xml['weather']['temperature']['_text'])) $data['temperature'] = $xml['weather']['temperature']['_text'];
if (isset($xml['weather']['wind']['_text'])) $data['wind'] = $xml['weather']['wind']['_text'];
if (isset($xml['weather']['humidity']['_text'])) $data['humidity'] = $xml['weather']['humidity']['_text'];
if (isset($xml['weather']['pressure']['_text'])) $data['pressure'] = $xml['weather']['pressure']['_text'];
if (isset($xml['weather']['visiblity']['_text'])) $data['visiblity'] = $xml['weather']['visiblity']['_text'];
return $data;
}
}
?>
lib/weatherRequest.php
Notice that the name of our parser method is "_parseFormat" followed by the mimetype of our input format (with slashes and pluses etc. removed).
So now, with our new WeatherRequest object is tow, we can PUT a application/weather+xml
document to our weather resources and update it's values.
An HTML interface
Okay, so we've got our XML weather service which is great for robots, but now we want to allow humans get in on the action too, so we want to add a HTML interface.
Adding a HTML representation for GETting is easy:
class: SmartyResource
mimetype: text/html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Weather for {$resource->location|escape}</title>
</head>
<body>
<h1>Weather for {$resource->location|escape}</h1>
<dl>
<dt>Summary</dt><dd>{$resource->summary|escape}</dd>
<dt>Temperature</dt><dd>{$resource->temperature|escape}</dd>
<dt>Wind</dt><dd>{$resource->wind|escape}</dd>
<dt>Humidity</dt><dd>{$resource->humidity|escape}</dd>
<dt>Pressure</dt><dd>{$resource->pressure|escape}</dd>
<dt>Visiblity</dt><dd>{$resource->visiblity|escape}</dd>
</dl>
</body>
</html>
resources/weather.html
And we need to add it as another representation of our data in our dispatcher:
// create a request object based upon the incoming HTTP request
$request =& new Request();
// create persistence adapters
$adapter =& new FileAdapter($mimetypes, 'resources');
$weatherAdapter =& new WeatherAdapter($mimetypes);
// load resource forcing a particular representation
$resource =& $request->load($weatherAdapter, array(
TONIC_FIND_FORCE_METADATA => array(
'representation' => array(
'/weather.xml',
'/weather.html'
)
)
));
// load the representation resource
if ($resource && $representation =& $resource->loadRepresentation($adapter)) {
$response =& $representation->get($request);
}
// output the response doing encoding as required
$response->output();
dispatch.php
So now we can access /london.html and receive our HTML representation
instead of the XML response.
Due to Tonics support for content negotiation, just requesting /london
will cause Tonic to provide the best representation as described by the Accept header
sent with the request.
To provide a way to update the weather via HTML we need to do a little work, since
HTML has no support for HTTP PUT and Web browsers don't understand our XML format.
Firstly we need to build an HTML form representation that allows a user to update
the weather data, and secondly a resource class with a post() method
that handles the posted form data and updates our weather resource.
Firstly the form. We create a resource on the file system and then adjust our dispatcher to route requests to /edit.html to the file adapter rather than our weather adapter:
mimetype: text/html
class: WeatherEdit
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<title>Edit weather</title>
</head>
<body>
{if !$smarty.get.location}
<form action="edit.html" method="get">
<label>Location:
<select name="location">
<option value="london">London</option>
<option value="edinburgh">Edinburgh</option>
</select>
</label>
<input type="submit">
</form>
{else}
<form action="edit.html" method="post">
<input type="hidden" name="location" value="{$smarty.get.location|escape}">
<label>Summary:
<input type="text" name="summary" value="{$weather->summary|escape}">
</label>
<label>Temperature:
<input type="text" name="temperature" value="{$weather->temperature|escape}">
</label>
<label>Wind:
<input type="text" name="wind" value="{$weather->wind|escape}">
</label>
<label>Humidity:
<input type="text" name="humidity" value="{$weather->humidity|escape}">
</label>
<label>Pressure:
<input type="text" name="pressure" value="{$weather->pressure|escape}">
</label>
<label>Visiblity:
<input type="text" name="visiblity" value="{$weather->visiblity|escape}">
</label>
<input type="submit">
</form>
{/if}
</body>
</html>
resources/edit.html
// create a request object based upon the incoming HTTP request
$request =& new Request();
/*
create the default persistence adapter to grab our resources from, here we'll
use the file system and point it to the directory named "resources"
*/
$adapter =& new FileAdapter($mimetypes, 'resources');
if ($request->url == '/edit.html') {
$resource =& $request->load($adapter);
} else {
/*
create an instance of our weather adapter that loads data from our weather service
*/
$weatherAdapter =& new WeatherAdapter($mimetypes);
/*
load the resource mentioned in the request via the request URL and accept headers
*/
$resource =& $request->load($weatherAdapter, array(
TONIC_FIND_FORCE_METADATA => array(
'representation' => array(
'/weather.xml',
'/weather.html'
)
)
));
}
dispatch.php
So now we have a HTML edit form, we need to load it with data and handle it's
POSTed response. Notice that we gave our edit resource a PHP class of WeatherEdit,
so lets create that class:
<?php
class WeatherEdit extends SmartyResource {
function &get(&$request) {
if (isset($_GET['location'])) {
$weatherAdapter =& new WeatherAdapter($request->mimetypes);
$weather =& Resource::find($weatherAdapter, '/'.$_GET['location']);
$this->_smarty->assign('weather', $weather);
}
return parent::get($request);
}
function &post(&$request, &$adapter) {
if (isset($_POST['location'])) {
$weatherAdapter =& new WeatherAdapter($request->mimetypes);
$weather = new Weather($weatherAdapter, $_POST);
$weather->set('url', '/'.$_POST['location']);
if ($weather->save()) {
return new Response(302, NULL, array(
'Location' => $request->rootUrl.'/'.$_POST['location']
));
}
}
return new Response(500);
}
}
?>
lib/weatherEdit.php
We've created a new get() method that if given a location
querystring parameter will load the data for the given location and assign it to
Smarty. Our resource body then uses that data to pre-populate the edit form with
the current data.
We've also created a post() method. It creates a new Weather resource
based on the POSTed data and saves it to the weather adapter. To keep the example short
we haven't done any sanity checking of the input, but if you wanted some you could
add it here or within the weather adapter itself depending on your needs.
On successful saving of the resource, we issue a 302 redirection header as the response. If somethin goes wrong, we simply return a HTTP 500 response.
Conclusion
So we've seen how to create a simple RESTful application using Tonic; using the file system adapter, writing our own persistence adapter to talk to our own data source, writing resource classes, providing a variety of representations, and writing a traditional HTML interface in a RESTful way.
Download the source files for this tutorial. Back to documentation home