PHP Zend Framework REST Server Tutorial under Apache + Linux

Submitted by nigel on Sunday 15th August 2010

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.

Proof of Concept

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>
http://localhost/api2/index.php?method=junk
<junk generator="zend" version="1.0">
  <response>
    <message>Unknown Method 'junk'.</message>
  </response>
  <status>failed</status>
</junk>
http://localhost/api2/index.php?method=sayHello
<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.

Solution

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>
http://localhost/api2/index.php?ListTeams
<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>
Sorted! Yippee!
blog terms
PHP Apache Linux