Rolling out an API for your cakePHP app Part ++1: The Solution

by frank on April 30, 2010

in PHP

In the previous post I covered creating a basic API using cakePHP and what limitations are imposed if you want to extend the API over the course of several versions. This post is going to cover the architecture of a possible solution, I don’t claim its the best solution out there and feedback is welcomed.

A quick rundown of the goals for this solution:

  • Contain a single version of the API to one file/class to improve maintenance
  • Orderly folder structure for API view files
  • Simple and minimal routing rules
  • A more DRY solution minimising code (and file) replication
  • Easy deployment of new API versions
  • A system to return API specific errors
  • A place for non object specific API methods


Overview of Solution Architecture

I created an API component to dispatch API requests to an API class in the vendors folder. Based on the version requested the component dispatches the request to the corresponding class, if it exists. That class then dispatches the request to the corresponding method, based on naming convention. The class also takes care of locating the correct view to use for each method/API version/response type combination, making use of the RequestHandler. The different API version classes can be extended so that a new version can inherit the functionality of the previous without the need to replicate methods.
Without further ado…

Routing

The routing rules are going to be very simple, one rule to rule them all and in the darkness bind them:

Router::connect('/:version/:controller/:action/*', array('prefix' => 'api', 'api' => true), array('version'=>'[0-9]+\.[0-9]+'));

This rule should route any URLs like:

http://yoursite.com/0.2/posts/get.xml

http://yoursite.com/10.3/posts/get.xml

To the posts controller api_get() method. The posts controller should include the API component in order to execute the following:

var $components = array('Auth', 'Acl', 'RequestHandler', 'Api');
...
function api_get() {
    $this->Api->dispatch();
}

The API Component

App::import('Vendor', 'Api_0_2', array('file' =>'api'.DS.'0.2'.DS.'api.php'));
class ApiComponent extends Object
{
    public $api = null;
 
    /**
     * Initialize API component.
     * Called before Controller::beforeFilter()
     *
     * @param $controller
     * @param $settings
     * @return void
     */
    function initialize(&$controller, $settings = array()) {
        $this->controller =& $controller;
    }
 
    /**
     * Dispatch the API request to the correct API class depending on version
     *
     * @return void
     */
    function dispatch() {
 
        //Want to dispatch to correct method in API class, get the name of the class
        $className = 'Api_'.str_replace('.', '_', $this->controller->params['version']);
 
        //Confirm class exists
        if (class_exists($className)) {
            $this->api = new $className();
        }
        else {
                //No need to worry about apiErrors just at the moment
            $this->cakeError('apiError', array('apiErrorCode'=>1001));
        }
 
        //Pass in controller
        $this->api->dispatch($this->controller);
    }
}

In this component I include the various API version classes that need to be instantiated for each version of the API that is requested. Once the correct class has been created dispatch() in that class is invoked.

The API Classes

The API classes all extend one central API class which lives at vendors/api.php.

<?php
App::import('Inflector');
class Api extends Object
{
    public $controller;
    public $version = null;
    public $rendered = false;
 
    /**
     * Error codes for API errors
     */
    private $errorCodes = array(
        1001 => array('httpCode'=>500, 'errorMessage'=>'Version does not exist.'),
        1002 => array('httpCode'=>500, 'errorMessage'=>'Unknown output file.'),
        1003 => array('httpCode'=>500, 'errorMessage'=>'Unrecognised method.'),
    );
 
    /**
     * Retrieve data relating to an API error code
     * 
     * @param $errorCode Int API error code
     * @return Array Data associated with the error code.
     */
    public function getError($errorCode) {
        if (in_array($errorCode, array_keys($this->errorCodes))) {
        	return $this->errorCodes[$errorCode];
        }
    	return false;
    }
 
    /**
     * Render the API request using a version to select the view.
     * 
     * @return void
     */
    function render() {
 
        $controller = strtolower(Inflector::underscore($this->controller->name));
        $version = $this->version;
        $folder = $this->controller->RequestHandler->prefers();
        $view = str_replace('api_', '', $this->controller->action);
 
        //Check file exists before rendering
        $viewFile = new File(VIEWS . "/$controller/api/$version/$folder/$view.ctp");
 
        if ($viewFile->exists()) {
        	$this->controller->render("/$controller/api/$version/$folder/$view");
            $this->rendered = true;
        }
        else {
 
            /**
             * Try to get previous missing view from previous API version
             * Perhaps this is not the best approach? But allows us to reuse earlier API version 
             * views without duplicating files for new API releases.
             */
            $version = substr(strstr($version, '.'), 1);
 
            //NOTE: the version of our API actually started at 0.3, so you might need to edit some of the for loops in the code for your particular case
            for ($i = $version; $i >= 3; $i--) {
            	$viewFile = new File(VIEWS . "/$controller/api/0.$i/$folder/$view.ctp");
                if ($viewFile->exists()) {
                    $this->controller->render("/$controller/api/0.$i/$folder/$view");
                    $this->rendered = true;
                    break;
                }
            }
        }
        if (!$this->rendered) {
        	$this->cakeError('apiError', array('apiErrorCode'=>1002));
        }
    }
 
    /**
     * Dispatch the API request to the corresponding method.
     * 
     * @param $controller Object Controller of original request
     * @return void
     */
    function dispatch(&$controller) {
 
        $this->controller =& $controller;
 
        $controllerName = $this->controller->name;
        $actionName = $this->controller->action;
 
        $functionName = Inflector::variable(str_replace('api', Inflector::variable($controllerName), $actionName));
 
        //Check that method exists
        if (method_exists($this, $functionName)) {
        	$this->$functionName();
        }
        else {
        	$this->cakeError('apiError', array('apiErrorCode'=>1003));
        }
 
        //Render if it hasn't already done so
        if (!$this->rendered) {
            $this->render();
        }
    }
}
?>

The API error related code is not important at this stage, I’ve included it for simplicity. Most important to note that dispatch() executes a method based on the request and render() grabs the correct view also based on that request. The render() method requires the RequestHandler component to be activated for the controller that is stored in $this->controller.

Also, render() will look for view files in the previous versions of the API, if for instance get.ctp is missing for the 0.3 API, render() will look to use the get.ctp file from API 0.2. I will get into the structure of view files later and this will make more sense. I’m not entirely sure its a good idea to do this, but it saves us from creating a whole new set of view files for each new version of the API.

The versioned API classes that extend the base class live at vendors/0.2/api.php substituting 0.2 for each particular version obviously. Each version has control over which methods it implements and how, each new version can extend the previous version so that all previous API methods are inherited.

<?php
App::import('Vendor', 'api/api');
class Api_0_3 extends Api  
{
    public $version = 0.3;
 
    function postsGet() {
        //Get posts, store them in an array and set them for the view?
        //Whatever code would go into the api_get() method in the posts controller, should go in here
        //With only one minor alteration, calls to $this-> should be replaced with $this->controller->
 
        $posts = $this->controller->Post->find('all', array(
            'contain'=>false
        ));
        $this->controller->set(compact('posts'));
    }
}

The Views

The view folder structure is fairly straight forward, the views continue to be stored in the views folder for the corresponding controller and continue to use the folder naming convention for the RequestHandler component:

http://yoursite.com/0.2/posts/get.xml = /views/posts/api/0.2/xml/get.ctp

http://yoursite.com/0.2/posts/get.json = /views/posts/api/0.2/json/get.ctp

http://yoursite.com/0.3/posts/get.xml = /views/posts/api/0.3/xml/get.ctp

If when creating a 0.3 version of the API you want to keep the same view that was used in 0.2 you do not need to create a new view file specifically for the 0.3 version, the 0.2 version of the file will be rendered automatically (see the API class render() method above). I’m not certain this is the best idea, but in the mean time it minimises replicating views unnecessarily.

Was this article useful?

rss feed icon

Email this article to yourself or...

rss feed icon

Subscribe to the RSS feed for more useful articles and tips.

Share this article with others

  • del.icio.us
  • Twitter
  • Reddit
  • StumbleUpon
  • Facebook
  • Digg
  • Steve

    Frank,

    Thanks a bunch for this code, very interesting idea! I’m just a little confused about one thing… It seems that this will keep controller-action logic and views separated into versions, but what about Models and database versioning? The posts controller in your example would still be tied to its Model regardless of version. Am I correct, or am I missing something?

    Thanks again.
    Steve

  • frank

    Hey Steve,

    Basically all I am versioning is the API with this solution. You’re correct, only the controllers and views are versioned – thats all I’m concerned about regarding the API. Model and database versioning is not really an issue for us and kind of outside the scope of this work on the API – I haven’t even thought about versioning either model or database tbh :-)

    HTH
    F.

  • Steve

    Frank,

    Thanks for responding! I really do like what you’ve done. I was also wondering if you implemented any special authentication for this API. I’m building an API right now and, I’m really stuck on authentication… It looks like the best CakePHP has built in is digest, but what about something like what Amazon does: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTAuthentication.html , or maybe oAuth? If you could offer any advice, I would greatly appreciate it!
    Thanks again,

    Steve

  • frank

    Hey Steve,

    At the moment our authentication is very basic, we just test an apikey passed in the GET string in the app_controller before filter. At some point I was thinking about using oAuth but thats about as far as I have got on it.

    Cheers,
    Frank.

  • Steve

    Frank,

    Thansk again for the resonse.

    Steve

  • Bart

    Hi Frank,

    thanks for the inspiration. Versioning was just the thing missing in my api. However, upon implementation of your solution, I keep getting the 1002-error: Unknown output file. As far as I can tell, all the files are in their right place.. Got any tips as to where I could find the solution?

    cheers,

    b

  • Bart

    Hi Frank. Never mind. I missed a double extension in my view.

  • bart

    I do however have one other question. How do you pass variables into the api? E.g. get /0.2/posts/get/1.xml with 1 being the $id of the post? They keep getting lost when I try to pass them into the api.

  • frank

    You have to do something like: /0.2/posts/get.xml?id=1 this solution wasn’t structured to allow passing args like you normally would in cakephp.

  • bart

    Hi Frank, I fixed it by adjusting the routing and slightly changing the api-code so the arguments are passed through in an array..

    // process arguments
    if (isset($this->controller->params['pass'])) {
    foreach ($this->controller->params['pass'] as $key=>$value) {
    $argument[$key] = $value;
    }
    }

  • Matt

    Frank, some brilliant stuff here. Your design is a great start for building my api architecture. I do have a question, though.

    Suppose your api interacts with a database. The database schema changes, and your new api reflects that. But your old api’s will no longer work properly, since the model has changed. Do you have a good solution for easily deprecating api’s when something like this occurs? Or must you manually go to each api_x_x class whose version is no longer compatible with the database and send back an error, like http 500 “version is deprecated”? This can get messy it seems.

    My problem is a bit more complicated then this, but this is a good start. My api is actually interacting with a java program, and so there are many changes that can be made to the java backend, such as format of return data and performance of the backend itself. The more potential changes the harder it is to do versioning for api’s it seems. Nevertheless, I think it should be possible :)

    Thanks in advance! Great code again

    Matt

  • frank

    Cheers Matt, short answer is: no. I don’t have a good solution to reflect changes to the model in the API. What you suggest is pretty much it, you might be able to do some data checking in beforeFilter() or something like that and throw a 500 error there in a central location. Would be interested to read what you come up with

  • Matt

    Will keep you posted! This problem won’t arise for many months though :p Thanks again for the code