CodeIgniter REST
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;
This looks promising. I’m hacking it into my site this weekend. Thanks!
Comment by Brad — October 17, 2008 @ 4:50 am
Around line 226, you forgot to delete:
echo ‘match!’;
Otherwise, it hasn’t affected my site. Still have to rewrite my controllers to make use of it.
Comment by Brad — October 17, 2008 @ 11:23 pm
woops! fixed that. thanks brad! NOT affecting your site is a very good thing. The idea was to be as unintrusive as possible…
Comment by maulin — October 20, 2008 @ 10:32 pm
Great work! I appreciate the time you’ve spent on this.
I had to change the uri_protocol to “REQUEST_URI” to get the find($query) method to function properly. I had it set to “AUTO” and whenever I specified parameters it would route the request through the findAll() method instead of find().
I am using CI version 1.7.0 if that makes any difference.
The other methods seem to work just fine.
Comment by ishmael — October 31, 2008 @ 5:08 pm
This is great. Can it be used to add sub resources, e.g.
friends/dave/
friends/dave/messages/
freinds/dave/messages/23/
Comment by Phil — November 8, 2008 @ 1:57 am
[...] options exist for PHP? CodeIgniter’s routing completely ignores the HTTP METHOD so there is serious hacking that needs to be done. Cake has REST support but wasn’t designed to make specifying useful [...]
Pingback by Towards RESTful PHP - 5 Basic Tips | Kris Jordan — December 2, 2008 @ 8:17 am
Nice plugin! yet I’m having some trouble using it. I hope you don’t mind but I have a few questions on how to go about using this in my existing application. In the rest.php config file let say for instance I have a controller Project.php, I have a method addProject() that creates a project from a form. How do I map that method to the rest.php config file in order to make it RESTful? or am I doing this wrong? Thanks!
Comment by Mathew — December 3, 2008 @ 4:32 am
This is good stuff! I’ll be looking at implementing this in a project soon.
Comment by Ed Finkler — December 12, 2008 @ 4:16 am
Hiya! Great work!
I got a problem when putting the files in place, but then found you are missing $route initialization in MY_Router.php. Just added these lines after line 82:
//initialize route
$route = array();
Then another question.. How do I make so that one URL responds to different formats, JSON, HTML, XML, etc.?
Thanks!!
Comment by Tair — December 19, 2008 @ 10:55 am
I can’t seem to get the find($query) option to work. I have a controller that works fine with the default findall behavior.
But when I try adding /?xyz=123 I get a 404 not found error.
Is there some other setting that I need to get this working? I’ve tried some of the other things that folks have left in comments above with regards to the REQUEST_URI etc…
Please help
Comment by Andrew — February 13, 2009 @ 9:26 pm
I’m having a similar issue with $query on $controller->find($query) method. I had to set $config['uri_protocol'] = “REQUEST_URI” or else I get a missing parameter. With it set, the function executes fine, but $query is not an associative array, it is still the string ‘?xyz=123′.
Would really love to make use of this library, and thanks for the work you have done so far.
Comment by Jack — February 25, 2009 @ 11:45 pm
@Jack I think that’s intended behavior. You can use native PHP functions to break it up into an associative array from inside the method if that is what you need.
Comment by nocash — March 5, 2009 @ 1:18 am
Thanks for this!
I might be thinking about this incorrectly, but I’m trying to allow for jsonp with a callback supplied, but after the routing, can’t seem to access $_REQUEST["callback"]; When doing findAll(); It occurs to me that the params are not being passed as I am not filtering my query (which makes sense), but I’m not sure where I can capture the callback for me to pass back with my json.
Cheers,
Nick
Comment by Nick — May 29, 2009 @ 11:37 am
@Andrew: I had the same issue (with URL query strings and getting 404s). My problem was that the URL of my application in config.php used a symlink, which confuses the CodeIgniter router library when using REQUEST_URI. Once I changed that to the actual path, everything worked fine.
Comment by nocash — June 13, 2009 @ 6:17 pm
Thanks for sharing. Very useful info. I’ll save this for future reference
I have in mind something like that for my time tracking web application.
Comment by Julian — September 2, 2009 @ 3:27 am
Anybody found a solution for the non-functioning find() issue?
When I set $config['uri_protocol'] to “AUTO” in config.php it redirects to findAll()
When I set $config['uri_protocol'] to “REQUEST_URI” in config.php it redirects to 404
I tried to set $config['base_url'] from a url (127.0.0.1/mysite/) to an absolute file path (ie., /var/www/mysite/) but didn’t work either
I have CodeIgniter_1.7.2
could appreciate any help… thanks!
Comment by arbie samong — September 15, 2009 @ 9:03 am
re: find() 404s and/or redirects to findAll()
I just made sure I don’t have the ‘index.php’ using .htaccess in my URI requests and it worked just fine. Plus set $config['uri_protocol'] = “REQUEST_URI” in config.php
http://codeigniter.com/forums/viewthread/69506/P0/
Comment by arbie samong — September 16, 2009 @ 7:28 am