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
Created Jan 19, 2008, last modified Jan 19, 2008