I have been looking into various ways of building a simple PHP JSON REST server (to connect easily to dojo’s JsonRestStore on the clien). First off, I am a bit surprised that this does not exist as a project somewhere already.
CakePHP has a pretty simple way of mapping your REST requests to its controllers. I like their general approach here. And overall I like CakePHP’s architecture, separation of concerns, and design. I am not a huge fan of their ORM (I prefer to declare my model in my code and have it work back to my database, not the other way around), but it gets the job done for simple applications. It is also fairly easy to change your view depending on the incoming request accepted content-types, so you could route to a JSON or XML view depending on the type of REST request.
But, I also have some problems with Cake. I find it can be maddening to debug at first — trying to figure which magically called view or method or template or htaccess redirect is causing your problem. But once you get it all configured, its acceptable. Mind you, you will have to handle your own response codes, json handling, and raw POST data, but that’s not too hard either.
The biggest problem with Cake is that I can’t easily use it with the Doctrine ORM. And I already have a working project using Doctrine for its model and data access layers. So my options were figuring out how to map my existing model to Cake or how to make a Doctrine-friendly framework use REST. I chose the latter. Since this project was way to small to warrant diving into Symfony, I chose CodeIgniter as my framework.
CodeIgniter seems small and relatively lightweight, and in all honesty, it is very similar to Cake. I think it doesn’t try to do quite as much as cake in the background, and that made it easier for me to immediately grasp. While it took the better part of 3 days to get my hands around Cake, it only took about 3 hours to get my hands wround CI. Heck, in 3 hours, I felt good enough about it, that I was ready to hack it to get REST to work with it.
There is one CodeIgnite/REST solution on their wiki, it is relatively complicated and I think intrusive into your controller code. In my mind, the combination of REQUEST_METHOD, REQUEST_URI and perhaps HTTP_ACCEPT headers should route you to the right controller/method to get on with your business. Actually I think that METHOD/URI route you to the controller/method and the HTTP_ACCEPT can modify your choice of view (since the controller/method should be the same regardless of how the data gets rendered).
Both Cake and CodeIgniter have a method for creating custom routing of URI to controller/methods. However, Cake’s is much more robust. CI basically only allows routing from URI without any analysis of the REQUEST_METHOD, and furthermore, disallows query-strings when used in conjunction with their friendly URL system. That is to say this would not be a valid URI in CI:
http://medryx.org/friends/?city=Portland
The good news is that CI allows overriding of its code libraries quite easily (as does Cake). You simply create a prefixed version of the class in your local application/libraries folder. So we just need to override the custom routing behavior and we can get REST in CI fairly easily.
First, I created a subclass of Router called MY_Router and placed it in MY_Router.php in my application/libraries folder. Then I found the meat of the Routing occured in _parse_routes, so I copied it, put it in my subclass, and started to hack.
I think the simplest way to allow for the REQUEST_METHOD to influence the route is to make the routes.php config file allow for this. By default, routes.php in the config directory is very simple, essentially a uri is compared against the key of the $route[] config variable, and if it exists (with some Regex niceness), then the route is sent to the string that is the value of the key. Its all explained quite simply in the CI docs.
So my plan is to let the value of a route be an associative array — the keys will be ‘GET’, ‘POST’, ‘PUT’, and ‘DELETE’. Then the value is the actual route. I’ll preserve the other regex goodness that CI already provides. It turned out to only add 20 lines of code to the original _parse_routes method to do this.
//if the route is not a plain string, but an array
if (is_array($this->routes[$uri])) {
//loop through the options and see if we find a match
foreach ($this->routes[$uri] as $method => $route) {
foreach ($val as $method => $route) {
if (strtolower($method) == strtolower($_SERVER['REQUEST_METHOD'])) {
$this->_set_request(explode('/', $route));
return;
}
}
}
}
}
Well that’s not too bad. That just gets added ~line 28, and a similar bit arond line 53, and you are done. Now its time to make our new custom routes. First lets look at a call to /friends:
$route['friends'] = array(
'GET' =>'friends/findAll',
'POST'=>'friends/add'
);
So, as per REST, a POST is a request to create new record, and in JSON-REST the RAW POST data is the content of the new record. A GET is a request for all records.
Now lets add PUT/DELETE methods, and allow for GET of a friend with a given id:
$route['friends/(:num)'] = array(
'GET' =>'friends/findById/$1',
'POST' =>'friends/update/$1',
'PUT' => 'friends/update/$1',
'DELETE' => 'friends/delete/$1'
);
That’s not too bad either. It uses the CI router regex stuff to get the id routed right to the right method as its parameter. In the case of POST/PUT, once again, the raw post data is the data.
The last bit turned out to be just a little trickier. It turns out that you can send parameters to the GET request, instead of an id, and that these params represent the query you want to execute. For example:
http://www.medryx.org/friends/?city=Portland
Well, this shouldn’t be too bad either — since we know that really any regex works as the pattern we can do:
$route['friends/\?(:any)'] = array(
'GET'=> 'friends/find/$1'
);
and that should do it right? Well, not quite. It turns out that when parsing the URI, CI by default uses an “AUTO” method, which, if there is a query-string, uses that as the URI. Woops, that’s not going to work for us. Luckily its a simple config to change it in config.php:
$config['uri_protocol'] = "REQUEST_URI"; //changed from "AUTO"
Ok, now we’re in business.
Update: Actually, all we need to do is make sure it doesn’t use the $_GET shortcut with the AUTO setting. You can see how I do this below.
Well still not quite. For security (I suppose), CI by default disallows ?, &, and = in the URI. So we need to allow that. Another simple config:
$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-\?\=\&'; //added ?, =, & to the end
Okay, now we really are in business. Fire this up, and it all “just works”. Next step is to create simple way to designate a controller as a REST controller, and if it is a REST controller, automatically handle all this custom routing, so you don’t have to repeat it all. But that is fun for another day.
Update
So giving this just a couple more minutes of thought, I think I’ve made it light years easier. Now you simple place MY_Router.php in your application/libraries folder, and rest.php in your application/config folder and you are DONE. (No need to change any configs, or edit any of the CI source).
Once setup, you get the following behavior:
GET:
http://www.medryx.org/Friends/100
This will call Friends controller in your controllers folder with the method findById(100). Better yet a POST or PUT to this same url will results in a call to your controller’s update method, with two parameters — the id (100), and the raw post data (which is what you need for REST). If, on the other hand this is a REST query in the form of:
GET:
http://www.medryx.org/Friends/?city=Portland
then your controller gets called with
$controller->find($query); //$query is assoc array or in this case array('city' => 'Portland')

The revised source for MY_Router.php, to be copied into your libraries folder is:
<?php
/**
* Drop this class into you CodeIgniter application/libraries folder to automatically provide REST
* routing to your application.
*
* This looks for a config/rest.php file that contains a list of controllers that you want to make
* available via REST. See that file for a full description of how it works.
*
* As an aside, this class provides a couple other nuggets: it makes url's case insensitive, and
* it provides a way for you to expose other web services only when the incoming request is
* of a certain method. For example, GET methods should not have side-effects, POST can, so you
* can make your web services more clear if you use those semantics. (even outside of REST)
*
* Keep in mind that using this Router will AUTOMATICALLY CHANGE YOUR CONFIG FOR THE FOLLOWING
*
* $config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-\?\=\&';
* $config['uri_protocol'] = "REQUEST_URI";
*
*
*
*/
class MY_Router extends CI_Router {
function MY_Router() {
$this->config =& load_class('Config');
//fix the config
$this->config->set_item('permitted_uri_chars', 'a-z 0-9~%.:_\-\?=&\*');
if ($this->config->item('uri_protocol') == 'AUTO') {
$_GET = array();
}
parent::CI_Router();
}
/**
* used to curry the parameters sent to your REST methods
*
* @param array $segments
* @param string $type (put, post, get, delete)
*/
function _set_rest_request($segments = array(), $type) {
parent::_set_request($segments);
$type = strtolower($type);
//this is our chance to add arguments
//for post/put lets provide the raw post
//data as an extra parameter
if ($type == 'post' || $type == 'put') {
$this->uri->rsegments[] = file_get_contents("php://input");
} else if ($type == 'get') {
//if there is an '=' in the final segment, then this is a url-encoded query string
//query, lets parse it
$seg = $this->uri->rsegments[count($this->uri->rsegments)-1];
if (preg_match('/=/', $seg) && preg_match('/\?/', $_SERVER['REQUEST_URI'])) {
parse_str($seg, $params);
$this->uri->rsegments[count($this->uri->rsegments)-1] = $params;
}
}
}
/**
* reads the rest.php file and sets up routing as appropriate
*
*/
function setupRestRouting() {
define('DEFAULT_REST_METHODS', 'DEFAULT_REST_METHODS');
$defaultRestMethods = array(
'find' => 'find',
'findById' => 'findById',
'update' => 'update',
'delete' => 'delete',
'add' => 'add',
'findAll' => 'findAll'
);
//load the rest config
@include(APPPATH.'config' . DIRECTORY_SEPARATOR .'rest'. EXT);
$rest = ( ! isset($rest) OR ! is_array($rest)) ? array() : $rest;
foreach($rest as $controller => $methods) {
if ($methods == DEFAULT_REST_METHODS) {
$methods = $defaultRestMethods;
}
$controller = strtolower($controller);
if (isset($methods['find']) && $methods['find']) {
$route[$controller . '/?\?(:any)'] = array(
'GET'=> $controller . '/' . $methods['find'] . '/$1'
);
}
if (isset($methods['findById']) && $methods['findById']) {
$route[$controller . '/(:num)']['GET'] = $controller . '/' . $methods['findById'] . '/$1';
}
if (isset($methods['update']) && $methods['update']) {
$route[$controller . '/(:num)']['POST'] = $controller . '/' . $methods['update'] . '/$1';
$route[$controller . '/(:num)']['PUT'] = $controller . '/' . $methods['update'] . '/$1';
}
if (isset($methods['delete']) && $methods['delete']) {
$route[$controller . '/(:num)']['DELETE'] = $controller . '/' . $methods['delete'] . '/$1';
}
if (isset($methods['findAll']) && $methods['findAll']) {
$route[$controller]['GET'] = $controller . '/' . $methods['findAll'];
}
if (isset($methods['add']) && $methods['add']) {
$route[$controller]['POST'] = $controller . '/' . $methods['add'];
}
//default for extra methods
$route[$controller . '/(:any)'] = $controller . '/$1';
}
$this->routes = array_merge($this->routes, $route);
}
/**
* override _set_routing to allow us to setup our REST routing
* before we parse the routs
*
*/
function _set_routing() {
// Are query strings enabled in the config file?
// If so, we're done since segment based URIs are not used with query strings.
if ($this->config->item('enable_query_strings') === TRUE AND isset($_GET[$this->config->item('controller_trigger')]))
{
$this->set_class(trim($this->uri->_filter_uri($_GET[$this->config->item('controller_trigger')])));
if (isset($_GET[$this->config->item('function_trigger')]))
{
$this->set_method(trim($this->uri->_filter_uri($_GET[$this->config->item('function_trigger')])));
}
return;
}
// Load the routes.php file.
@include(APPPATH.'config/routes'.EXT);
$this->routes = ( ! isset($route) OR ! is_array($route)) ? array() : $route;
unset($route);
$this->setupRestRouting();
// Set the default controller so we can display it in the event
// the URI doesn't correlated to a valid controller.
$this->default_controller = ( ! isset($this->routes['default_controller']) OR $this->routes['default_controller'] == '') ? FALSE : strtolower($this->routes['default_controller']);
// Fetch the complete URI string
$this->uri->_fetch_uri_string();
// Is there a URI string? If not, the default controller specified in the "routes" file will be shown.
if ($this->uri->uri_string == '')
{
if ($this->default_controller === FALSE)
{
show_error("Unable to determine what should be displayed. A default route has not been specified in the routing file.");
}
$this->set_class($this->default_controller);
$this->set_method('index');
$this->_set_request(array($this->default_controller, 'index'));
// re-index the routed segments array so it starts with 1 rather than 0
$this->uri->_reindex_segments();
log_message('debug', "No URI present. Default controller set.");
return;
}
unset($this->routes['default_controller']);
// Do we need to remove the URL suffix?
$this->uri->_remove_url_suffix();
// Compile the segments into an array
$this->uri->_explode_segments();
// Parse any custom routing that may exist
$this->_parse_routes();
// Re-index the segment array so that it starts with 1 rather than 0
$this->uri->_reindex_segments();
}
/**
* the meat of REST (and all) routing. if regular routing, this is the same as _parse_requests
* but this adds the REST capable routing, calls _set_rest_request instead of _set_request
* when appropriate to allow currying of params, and, as a bonus, builds in case-insenitivity
* so that the url typed into the browser can be of any case without any problems
*
*/
function _parse_routes()
{
// Do we even have any custom routing to deal with?
// There is a default scaffolding trigger, so we'll look just for 1
if (count($this->routes) == 1)
{
$this->_set_request($this->uri->segments);
return;
}
// Turn the segment array into a URI string
$uri = strtolower(implode('/', $this->uri->segments));
// Is there a literal match? If so we're done
if (isset($this->routes[$uri]))
{
if (is_array($this->routes[$uri])) {
//loop through the options and see if we find a match
foreach ($this->routes[$uri] as $method => $route) {
foreach ($this->routes[$uri] as $method => $route) {
if (strtolower($method) == strtolower($_SERVER['REQUEST_METHOD'])) {
$this->_set_rest_request(explode('/', $route), strtolower($_SERVER['REQUEST_METHOD']));
return;
}
}
}
}
$this->_set_request(explode('/', $this->routes[$uri]));
return;
}
$restRequest = false;
// Loop through the route array looking for wild-cards
foreach ($this->routes as $key => $val)
{
// Convert wild-cards to RegEx
$key = str_replace(':any', '.+', str_replace(':num', '[0-9]+', $key));
// Does the RegEx match?
if (preg_match('#^'.$key.'$#i', $uri))
{
if (is_array($val)) {
//loop through the options and see if we find a match
foreach ($val as $method => $route) {
if (strtolower($method) == strtolower($_SERVER['REQUEST_METHOD'])) {
$val = $route;
$restRequest = strtolower($_SERVER['REQUEST_METHOD']);
break;
}
}
}
// Do we have a back-reference?
if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
{
$val = preg_replace('#^'.$key.'$#', $val, $uri);
}
if ($restRequest) {
$this->_set_rest_request(explode('/', $val), $restRequest);
} else {
$this->_set_request(explode('/', $val));
}
return;
}
}
// If we got this far it means we didn't encounter a
// matching route so we'll set the site default route
$this->_set_request($this->uri->segments);
}
}
and the rest.php file to put into your application/config folder:
<?php
/**
* Drop this file into you CodeIgniter application/config folder to enable REST handling for
* your controllers. You also need to copy MY_Router.php into application/libraries.
*
* In this file you will specify which controllers will use REST handling, and how they
* will handle it. the DEFAULT_REST_METHODS are as follows:
*
* $rest['MyController']= array(
* 'find' => 'find',
* 'findById' => 'findById',
* 'update' => 'update',
* 'delete' => 'delete',
* 'add' => 'add',
* 'findAll' => 'findAll'
* );
*
* is the same as:
*
* $rest['MyController'] = DEFAULT_REST_METHODS
*
* the signature for these methods is as follows
*
* function find($query):
* $query: associative array of params sent to your query
* example: http://foo.com/restService/?category=true
*
* function findAll():
* example: http://foo.com/restService/
*
* function findById($id):
* $id: numeric id of your item (must be numeric)
* example: http://foo.com/restService/123
*
* function update($id, $data)
* $id: numeric id of your item (must be numeric)
* $data: the raw post data content that should be used to update (usually json or xml)
*
* function add($data)
* $data: the raw post data content that should be used to add (usually json or xml)
*
* function delete($id)
* $id: numeric id of your item (must be numeric)
*
* If you would like to map to different methods from each of these, you
* can simply provide the name you prefer in the declaration of your rest service below.
* if there are some messages you would like to ignore (404), then you
* can simply omit that declaration, or set the declaration to empty string/null/false
*/
$rest['Friends'] = DEFAULT_REST_METHODS;