The Zend Framework lends itself to web services due to the bundled classes which come with the Framework. It is ideal both for consuming services (client) and exposing services (server). With this tutorial we'll put together a working REST server using the Framework. If you haven't completed my earlier tutorial on creating a Zend Framework Hello World, you should read it now since we'll be using it as our starting point.
For those who haven't completed the earlier tutorial, make sure you:
1. Create a Zend Framework directory structure ready for a new project. I called mine api;
2. Added a VirtualHost in Apache and restarted the server;
3. Downloaded the latest stable Zend Framework release and installed in the library subdirectory;
4. Created the bootstrapper we used in the previous example.
Ok, we need our service to do something quasi useful to show how our server works. So it'll be a football team server with two methods - 1. to return a list of all English Premier League teams, and 2. to search for a particular football team, or teams. This enables us to show the separation of this 'Business Logic' which will be held in the 'Model' part of the 'MVC' - something we couldn't do in the simplistic Hello World example earlier. In line with convention, our REST server will return the output in XML format.
I'd better duplicate my structure and httpd.conf entries from api to api2 since I should retain api in case I need to refer back. It makes no odds, other than your urls will be slightly different contingent upon which you use.
The first thing we'll do is get a very basic server up and running as a proof of concept. Since we'll be using the built-in Zend_Rest_Server() class, a very basic working server can be up and running in a few minutes.
Ok, our very basic service provides a simple "Hello" and "Goodbye" response. First we need the bootstrap file.
api/index.php
<?php
$rdir = realpath(dirname('SCRIPT_NAME'));
set_include_path($rdir . '/library' . PATH_SEPARATOR . get_include_path());
try {
// use Autoloader to load the Zend classes
// instead of Zend_Loader::loadClass('Zend_Controller_Front');
// a smarter alternative for larger projects
require_once('Zend/Loader/Autoloader.php');
$autoloader = Zend_Loader_Autoloader::getInstance();
$autoloader->setFallbackAutoloader(true);
$fcontroller = Zend_Controller_Front::getInstance();
$fcontroller->throwExceptions(TRUE);
$fcontroller->setParam('noErrorHandler', TRUE);
$fcontroller->setControllerDirectory("$rdir/application/controllers");
$fcontroller->dispatch();
} catch (Exception $e) {
$contentType = 'text/html';
header("Content-Type: $contentType; charset=utf-8");
print 'An unexpected error occurred:';
print '<h2>Unexpected Exception: ' . $e->getMessage() . '</h2><br /><pre>';
print $e->getTraceAsString();
}
?>This is practically the same as our previous example, but we have tidied up the class loading by getting a Singleton instance of the autoloader class with Zend_Loader_Autoloader::getInstance();. For larger projects this makes sense rather than continually having to load classes manually with Zend_Loader::loadClass
Next we define the controller code which has similar structure to last time.
<?php
class IndexController extends Zend_Controller_Action {
protected $_server;
public function init() {
require_once('Zend/Rest/Server.php');
$this->_server = new Zend_Rest_Server();
$this->_helper->viewRenderer->setNoRender();
}
public function indexAction() {
$this->_server->setClass('ServiceSalutations');
$this->_server->handle();
}
}
class ServiceSalutations {
public function sayHello($name) {
return "Howdee $name, how are you?";
}
public function sayGoodbye($name) {
return "Goodbye $name and have a great day!";
}
}
?>The init() function is used to initialise the controller and is called before the preDispatch() hook, so this is the place to instantiate the server. You will also note we are explicitly saying we have no output script for this controller with the $this->_helper->viewRenderer->setNoRender(); call. If you neglect this, the controller will have a look around for a template file to render. In the IndexAction we are defining what our service will look like, and getting a handle to the service. The class ServiceSalutationscontains the two services we are offering - a message to say hello, and one to say goodbye. The REST server will automatically create the XML output, so we are ready to give it a spin! Type the following in your browser:
http://localhost/api2/index.php?method=sayGoodbye&name=badzilla
<ServiceSalutations generator="zend" version="1.0"> <sayGoodbye> <response>Goodbye badzilla and have a great day!</response> <status>success</status> </sayGoodbye> </ServiceSalutations>
<junk generator="zend" version="1.0"> <response> <message>Unknown Method 'junk'.</message> </response> <status>failed</status> </junk>
<ServiceSalutations generator="zend" version="1.0"> <sayHello> <response> <message> Invalid Method Call to sayHello. Missing argument(s): name. </message> </response> <status>failed</status> </sayHello> </ServiceSalutations>
That's the concept proven. However, there are a couple of 'I don't likes' about the solution:
1. I don't like the generator=zend text - firstly it is a distraction and will mean nothing to the end user of my service, and secondly a hacker gleans potentially important information about my server and may target vulnerabilities as a consequence.
2. I don't like the url structure - the method= is cumbersome and again signposts the fact we are using Zend.
Our fully-blown solution is below. The bootstrap is the same as our Proof of Concept, so I won't reproduce it in case I end up confusing you!
api2/index.php
As earlier. Cut and paste previous code
Next up is the index controller. This does have a few changes to consider.
api2/application/controllers/IndexController.php
<?php
class IndexController extends Zend_Controller_Action {
protected $_server;
public function init() {
require_once('Zend/Rest/Server.php');
$this->_server = new Zend_Rest_Server();
$this->_server->returnResponse(TRUE);
$this->_helper->viewRenderer->setNoRender();
}
public function indexAction() {
require_once('application/models/teams.php');
$this->_server->setClass('ServiceTeams');
$response = $this->_server->handle();
header('Content-Type: text/xml');
// replace generator="zend" with something more appropriate
echo str_replace('generator="zend"', 'generator="API2"', $response);
}
}
?>Some commentary is required here.
$this->_server->returnResponse(TRUE); I got the idea for this my scrutinizing the Zend/Rest/Server.php code. The returnResponse method sets a flag that prevents the server from automatically outputting the xml.
require_once('application/models/teams.php'); Our separation of the business logic from the controller necessitates the inclusion of the model code via a require_once statement.
$this->_server->setClass('ServiceTeams'); We are declaring our service here which will be defined in the model.
header('Content-Type: text/xml'); Since we are no longer reliant upon the Zend server to output the xml, we need to manually set the header.
echo str_replace('generator="zend"', 'generator="API2"', $response); and then output the server's xml but before we do, replace the zend string with API2. This could of course be any string you wish.
The model code contains the available service methods.
api2/application/models/teams.php
<?php
class ServiceTeams {
private $header = '<?xml version="1.0" encoding="UTF-8"?>';
private $teams = array('Arsenal', 'Aston Villa', 'Birmingham City', 'Blackburn Rovers', 'Blackpool', 'Bolton Wanderers',
'Chelsea', 'Everton', 'Fulham', 'Liverpool', 'Manchester City', 'Manchester United',
'Newcastle United', 'Stoke City', 'Sunderland', 'Tottenham Hotspur', 'West Bromwich Albion',
'West Ham United', 'Wigan Athltic', 'Wolverhampton Wanderers'
);
public function ListTeams() {
return $this->_constructOutput();
}
public function SearchTeams($search = '') {
return $this->_constructOutput($search);
}
private function _constructOutput($search = '') {
$count = 0;
$xml = $this->header;
$out = '<teams>';
foreach($this->teams as $team)
if ($search == '' or strstr($team, $search)) {
$out .= "<team>" . $team . "</team>";
$count++;
}
$out .= "<result>Found $count teams</result></teams>";
return simplexml_load_string($xml . $out);
}
}
?>Firstly, please note that a formatting bug is supressing the necessary single quote where $header is defined. If you directly copy this paste for your own app, don't forget to put it back in. Ok, now for some more explanation. You will see that the two publicly available methods are our service definition, SearchTeams and ListTeams. The method _constructOutput will create the xml output for the other two methods. Also worthy of note is the call to the built-in function simplexml_load_string which returns the output to the Zend server in the format it requires.
Finally, we need to use mod_rewrite to rewrite the URL sent to our server so we can live without the annoying method=. To do this, I have created a .htaccess file but you could just as easily put the commands into your httpd.conf file. The beauty of using .htaccess is there is no need for frequent Apache server restarts as we try and debug our statements.
api2/.htaccess
RewriteEngine On RewriteCond %{QUERY_STRING} !^method= RewriteCond %{QUERY_STRING} (.*)$ RewriteRule . http://localhost/api2/index.php?method=%1 [L]
We are saying here:
1. Look for any URL that doesn't contain the phrase method= in the query part of the URL;
2. In that circumstance, get the rest of the query string;
3. Add method= ahead of the rest of the URI.
Ok that should do it, so let's give it a spin. Point your web browser at the following locations:
http://localhost/api2/index.php?SearchTeams
<teams> <team>Arsenal</team> <team>Aston Villa</team> <team>Birmingham City</team> <team>Blackburn Rovers</team> <team>Blackpool</team> <team>Bolton Wanderers</team> <team>Chelsea</team> <team>Everton</team> <team>Fulham</team> <team>Liverpool</team> <team>Manchester City</team> <team>Manchester United</team> <team>Newcastle United</team> <team>Stoke City</team> <team>Sunderland</team> <team>Tottenham Hotspur</team> <team>West Bromwich Albion</team> <team>West Ham United</team> <team>Wigan Athltic</team> <team>Wolverhampton Wanderers</team> <result>Found 20 teams</result> </teams>
http://localhost/api2/index.php?method=SearchTeams&search=City This proves that full URIs are still working ok with the method= present.
<teams> <team>Birmingham City</team> <team>Manchester City</team> <team>Stoke City</team> <result>Found 3 teams</result> </teams>
<teams> <team>Arsenal</team> <team>Aston Villa</team> <team>Birmingham City</team> <team>Blackburn Rovers</team> <team>Blackpool</team> <team>Bolton Wanderers</team> <team>Chelsea</team> <team>Everton</team> <team>Fulham</team> <team>Liverpool</team> <team>Manchester City</team> <team>Manchester United</team> <team>Newcastle United</team> <team>Stoke City</team> <team>Sunderland</team> <team>Tottenham Hotspur</team> <team>West Bromwich Albion</team> <team>West Ham United</team> <team>Wigan Athltic</team> <team>Wolverhampton Wanderers</team> <result>Found 20 teams</result> </teams>