Jamie Rumbelow
THE CODEIGNITER HANDBOOK
Volume 2 - API Design
API Design Copyright ©2012 Jamie Rumbelow
All rights reserved. No part of this book may be reproduced without the prior written permission of the publisher, except for personal use and the case of brief quotations embedded in articles or reviews.
Every effort has been made to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the authors, Efendi Books nor its dealers or distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book.
Efendi Books has endeavored to provide trademark information about all companies and products mentioned in this book, however we cannot guarantee that this information is 100% accurate.
CodeIgniter® is a registered trademark of EllisLab, Inc. CodeIgniter® logo copyright 2007 - 2012 EllisLab, Inc., used with permission.
First Published: 2012 This Edition Published: 2012
eBook Version: 1.0.0
Published by Efendi Books
ISBN: 978-0-9571791-1-0
One should never read too much into a book's dedication.
Table Of Contents
Acknowledgments ............................................................................. i An Introduction To The CodeIgniter Handbook ................................ ii An Introduction To Volume Two .......................................................iii Who Should Read This Book? ...........................................................iv Part 1 - Theory An HTTP Primer............................................................... 1 Resources ....................................................................... 4 RESTful Routes ............................................................... 5 Part 2 - The Build Routing ......................................................................... 11 Responding................................................................... 17 Versioning..................................................................... 27 Error Handling .............................................................. 36 Part 3 - Authentication How We Authenticate ................................................... 44 Writing The Code .......................................................... 45 Throttling ...................................................................... 50 Part 4 - Debugging OPTIONS ....................................................................... 58 Debug Mode ................................................................. 68 Automated Testing ....................................................... 76 Summary ....................................................................................... 82
Acknowledgments First of all a stupdendously large thank you to everyone who bought Volume One. Your support, love and cold hard cash has been much appreciated and is the only reason I bother getting up in the morning.
A big thank you to everyone involved in the production of this book: Dean, Susie (for putting up with Dean), Laura for her excellent illustrations and Charlie, Matt and the team at Print GDS for doing a great job with the printing. Thanks must also go to all those who reported feedback and gave ideas from the last book–your Twitter conversations have all been useful, engaging and inspiring.
Thanks again to Mum, Dad, Joseph and my friends for dealing with me, emotionally, physically and socially. I’m a rather large malignant tumour on the otherwise clean artery walls of your lives and I commend you all for putting up with me. Thanks to EllisLab for CodeIgniter and the logo. Thanks to GitHub for being awesome. Thanks to my girlfriend for not existing. If she did I’d have no time to write these silly books.
Finally, a ridiculously large–for certain larger than the aforementioned tumour–thanks to Neil Davidson for all the support, advice, offers, introductions and random thoughts that have kept me going for the past few months. You are the greatest mentor I could have hoped for. And thanks for the free lunch.
i
An Introduction To The CodeIgniter Handbook I’ve been programming with CodeIgniter for six years. When I first downloaded the source code and dug into what would soon become the heart and soul of my professional career, I had an epiphany. Suddenly, I had found a framework that made sense from the word ‘go.’ It took me all of fifteen seconds to download the source and extract it into my Sites directory. From that point onward, my life as a developer had dramatically changed.
What really made CodeIgniter special was the sense of excitement that it invoked. If you’ve ever had the misfortune of meeting me, you know that the excitement is still there. I’m so passionate about CodeIgniter; it’s visible on my face and in my body language. Just as it is visible in the faces of every other developer I’ve ever met who has experienced the same epiphany.
I assume you’re reading The CodeIgniter Handbook because you’re passionate about CodeIgniter. You’re excited about the future of the framework. But more important, you’re excited about the present. About the sheer promise that CodeIgniter can bring to your applications. Despite all the hype around other frameworks and languages, CodeIgniter still remains the greatest PHP framework for any developer developing realworld applications. CodeIgniter provides pragmatism without overabstraction, speed without simplification, power without bulk. It’s the perfect mix of programming happiness and scalability. It’s eclectic.
ii
An Introduction To Volume Two In today’s world the focus of web development has firmly shifted from isolated systems held behind firewalls and paywalls to interconnected networks of applications, all talking to each other in real time. CodeIgniter’s flexibility makes it very easy to create Application Programming Interfaces (APIs) and allow your applications to share data and functionality.
In this book, you’ll learn the principles behind modern RESTful API design as well as a bunch of helpful implementation details, such as how to version your APIs, how to use HTTP to specify content types and how to extend and debug your APIs with useful homegrown tools.
CodeIgniter is lightweight, fast and malleable, and is thus the perfect framework to build extensable, efficient APIs in PHP.
iii
Who Should Read This Book? If you’re a novice to CodeIgniter, this book is for you. If you are an experienced CodeIgniter developer, this book is for you. As long as you understand the core concepts of Model-View-Controller and can code up a simple app, you’re bound to get something valuable from this text. I’ll assume that you understand the basic differences between libraries, helpers, models and views and how to use them within CodeIgniter.
This book was written with CodeIgniter version 2.1.2 and PHP 5.3. Any code examples here are tested to work with this CodeIgniter version, not any prior or future versions. Certain sections of this book use PHP5.3 only features such as namespaces. With the rapid pace of development, I can make no guarantee that code will work on a future version. Additionally, the final chapter uses PHPUnit 3.6.12. If you’d like to follow along, please use these respective versions.
iv
…in which we discuss the theoretical side of designing and building APIs. We’ll examine the principles of resources that make up the fundamental platform upon which REST is designed, we’ll take a look at the HTTP spec, how we can design our API’s URLs in a sensible and predictable fashion, and we’ll look at the importance of idempotence.
In volume one we discussed the basics of REST, resources and HTTP methods. Some of this chapter may be familiar to you already, particularly if you've read volume one. Feel free to skip this bit if you feel confident with HTTP methods and HTTP as a protocol itself.
Web browsers and web developers are generally only familiar with the two main HTTP methods, GET and POST. If you dig through the HTTP spec, you'll discover that these are only two of the nine methods available to HTTP clients:
2
•
GET is for retriving a resource
•
POST is for creating a new resource
•
PUT and PATCH are for updating an existing resource
•
DELETE is for deleting a resource
•
HEAD is identical to GET, except it will only return the headers rather than the resource itself
•
OPTIONS returns information about a URL; what parameters does the request take? What will it return? What other requests are available?
•
TRACE echos back the request. This is usually used for debugging.
•
CONNECT is for converting the request into a socket. This is particularly useful for proxying.
GET, HEAD, TRACE and OPTIONS are the four safe methods in HTTP. This means that they should cause no side effects and are only intended to retrieve information from the server and not modify any data or change the state. In practice, there are a series of relatively harmless side effects that you may want to make, such as logging, caching, or handling analytics.
On the flipside, POST, PUT, DELETE, CONNECT and PATCH are not safe, and are intended for requests that will modify state on the server (or another server, such as sending an email or making an external API request). These are methods that may require context in order to execute: the user may need to fill in a form, for instance.
The HTTP methods PUT and DELETE are also defined as idempotent, which means that multiple, identical requests should only affect the server once and thus have the same effect as a single request. The result may differ, but the state of the server should be identical every time.
POST isn't idempotent, so it can have multiple effects when you submit a request multiple times. This can be a problem; take, for instance, a payment processor. Submitting multiple requests to a payment processor will charge the customer's card multiple times. In order to get around this, it is the web application's responsibility to ensure that the user can't submit a request more than once.
3
HTTP is a stateless protocol, meaning the server doesn't have to remember anything about the request. Requests can be made at any time from anywhere and the server isn't required to track them. In order to get around this and facilitate user authentication, analytics, etc, we can use cookies, server-side sessions or access tokens.
All this information about HTTP might seem a bit too much, but understanding these simple concepts is incredibly valuable for designing RESTful APIs.
Resources The discerning reader may have noticed that I've been referring to resources rather regularly recently. In the context of a RESTful API, a resource is simply a thing: an element, a piece of data that we can access and manipulate.
A resource could be a book, a user, a category, a blog post, an item in a list or a process to apply to another resource. If you begin to think about your entire application in terms of resources, you can plan the routes and relationships between resources with a lot more accuracy and consistency.
Throughout this book, we'll be creating an API to track statistics for a website / application. We'll be able to arbitrarily create trackers, and those trackers will contain a bunch of different values. We could then write a frontend application to pull this data out and display it as a graph or visualisation.
Let's decide on a basic list of requirements our API will need to fulfil:
4
•
Create a new tracker
•
Create a new value within a tracker
•
Display all the values in a specific tracker
•
Update a tracker's name
•
Delete a tracker
How do these requirements translate into resources? Very simply!
We have a Tracker resource, which contains our tracker information. A Tracker can have multiple Value resources. Nice and simple, but this is a simple application. How do we 'resourcify' something less obvious?
Take authentication, for instance. When you log into a website or application, what you're really doing is creating a new Session resource. It might not seem obvious initially, but you can apply this logic to pretty much anything. A PasswordReset resource is another good example. These mightn't even be backed by a database; but you're still creating a resource.
RESTful Routes I've mentioned RESTful routes a couple of times already, but what on earth are they?
REpresentational State Transfer (REST) is a way of structuring a web service / API. REST is a simple, clean and elegant way of designing a web service around the concept of resources that we just talked about. Now we have a better knowledge of our HTTP methods and what they're used for, we can start to use the REST structure to design our routes.
The idea of a RESTful architecture is that each resource has a series of repeatable, generic, conventional routes based on a URI endpoint and the HTTP verb used to make the request. By combining these two elements, we can have a uniform set of routes that are predictable and consistent.
The standard set of RESTful routes are divided into two groups. Collection routes are routes (and associated functionality) that are applied to the entire set, group, collection of resources. Member routes, on the other hand, represent a specific resource within the collection.
5
Let's take a look at our routes for our Tracker resource. It's a convention that our controller + routes are in the plural–everyone who's read volume one knows how much I like conventions!–so our routes will be based off trackers:
Collection Routes
HTTP Method + URI
Result
GET /trackers
List all the trackers
POST /trackers
Create a new tracker in the collection
PUT /trackers
Replace the entire collection
DELETE /trackers
Delete the entire collection
Member Routes
HTTP Method + URI
Result
GET /trackers/:id
Get details about a specific tracker
POST /trackers/:id
Create a new collection based on that member
PUT /trackers/:id
Update the tracker
DELETE /trackers/:id
Delete the tracker
The Important Ones I've listed the entire set of methods above, but, in reality, there are very few occasions when you'll need to implement everything. There are a set of five staple methods that you'll almost always want to implement:
6
•
GET /posts
•
POST /posts
•
GET /posts/:id
•
PUT /posts/:id
•
DELETE /posts/:id
The others, such as replacing / deleting the entire collection and creating a new collection based on a specific member are fringe cases, and are thus very infrequently used.
Nesting As we discussed earlier, our Tracker resource can have multiple Value resources. This type of relationship is very easy to express as a URI. When we want to find the values for a Tracker, conceptually, we're narrowing down into the context of a specific Tracker and asking for all its values:
http://example.com/trackers/:id/values
Our /trackers/:id/values URL is a RESTful endpoint too; we'll need to be able to POST to create a new Value and GET the collection.
A good example I like to give is the classic blogging system; building it with RESTful URLs is a great test case for resource nesting. A Post has multiple Comment resources and each Comment could even have an Author resource:
http://example.com/posts/421/comments/1218/author
Nesting can be pretty much infinite; it's just a clever way of directly accessing the resource you want to get to. It's also much, much cleaner than the alternatives: including an id as a $_GET parameter or as a part of the resource itself. By using RESTful ideals we can create much cleaner relationships between our data.
7
Singular Resources I'll quickly touch on something that you might have noticed above. Sometimes, you'll have a resource that you only have one of, that is to say, you won't look up with an identifier. The Author case is a good example. A Comment will only ever have one Author; we won't ever need to look up a comment's authors or select a specific one. This means we don't need to pluralise and use the authors URI. Instead, we simply use author.
8
…in which we learn the best ways to integrate our knowledge of REST into CodeIgniter. We’ll take a look at the basics of getting data in and out of our API, how to keep our existing clients happy and retain backward compatibility with versioning and how best to throw HTTP friendly errors when things go wrong.
Before we begin to write our API, I'd like to make a personal mention to Phil Sturgeon's excellent CodeIgniter REST library[1]. Phil's library is easy to use and rich in features. I've used it for a tonne of different projects and the
1.https://github.com/philsturgeon/codeigniter-restserver
10
code is very well tested. It contains a bunch of sophisticated functionality such as API keys, logging and basic authentication.
So, then, why are we building our own system from scratch? Two reasons.
Firstly, I want a much higher level of control over the core processes and the request -> response cycle. When we look at error handling at the end of this chapter, for instance, we'll need to make a fair few changes to our code, and using Phil's library would prohibit this. Implementing properly RESTful routing and API versioning will be easier when it's our code and we're not having to work around Phil's. Phil's library is old and our system, while similar, will rethink many of the approaches and update them.
Secondly, we'll only learn how to do these kinds of things if we write them ourselves! By building the system from scratch we'll be able to really get to grips with the core concepts surrounding API development. By the end of this book we will have build a fairly abstracted system, so hopefully you'll have a drag-and-drop solution to use across multiple projects.
Routing We've taken a look at the theory of a set of RESTful routes and mapping a specific resource, based on HTTP method and URI endpoint. CI's routing mechanism is very rudimentary, and doesn't support routing based on HTTP method, so how do we implement this in CodeIgniter?
I've written a clever CodeIgniter routing library to simplify HTTP and RESTful routing, called Pigeon[2]. Pigeon is an intelligent little library that makes RESTful routing, nesting and other complex routing tasks easy. With Pigeon, our entire routing structure will look like this:
2.https://github.com/jamierumbelow/pigeon
11
Pigeon::map(function($r) { $r->resources('trackers', function($r) { $r->resources('values'); }); });
$route = Pigeon::draw();
Pigeon is intended to be the most declarative and natural way of building routes.
While Pigeon is the preferred solution, it's initially better to truly understand what's going on under the hood. We'll also want to extend our routing later on in the book, adding support for versioning and response formats.
Instead of using the easier solution, we'll work within the boundaries of what CodeIgniter offers us and build our routes from scratch.
Firstly, let's map our basic GET requests:
$route['trackers'] = 'trackers/index'; $route['trackers/(:any)'] = 'trackers/show/$1';
So far so good. We map /trackers straight through to Trackers::index and /trackers/something through to Trackers::show. Instantly, however, I can see a problem. We're still not distinguishing our routes based on the HTTP verb. Let's try again, this time using a switch statement to change the routing based on the method:
12
$route = array();
switch (strtoupper($_SERVER['REQUEST_METHOD'])) { case 'GET': $route['trackers'] = 'trackers/index'; $route['trackers/(:any)'] = 'trackers/show/$1'; break; }
Fantastic. Let's expand our switch statement out to support the POST method:
case 'POST': $route['trackers'] = 'trackers/create'; break;
Good good. Now for PUT and DELETE:
case 'PUT': $route['trackers/(:any)'] = 'trackers/update/$1'; break;
case 'DELETE': $route['trackers/(:any)'] = 'trackers/delete/$1'; break;
…and that is really all it takes to have basic RESTful route support in your CodeIgniter applications. Expanding it out to support our nested Value
13
resource is easy too – we just insert our nested resource before and ensure to nest it within the URI. For POST, for instance:
$route['trackers/(:any)/values'] = 'values/create/$1';
Our switch statement should now look like this:
switch (strtoupper($_SERVER['REQUEST_METHOD'])) { case 'GET': $route['trackers'] = 'trackers/index'; $route['trackers/(:any)/values'] = 'values/index/$1'; $route['trackers/(:any)'] = 'trackers/show/$1'; break;
case 'POST': $route['trackers'] = 'trackers/create'; $route['trackers/(:any)/values'] = 'values/create/ $1'; break;
case 'PUT': $route['trackers/(:any)'] = 'trackers/update/$1'; break;
case 'DELETE': $route['trackers/(:any)'] = 'trackers/delete/$1'; break; }
14
Another benefit to using CodeIgniter's routing in this fashion–where routes are only defined when the method is being requested–is that if someone tries to request a route outside of the method CodeIgniter will throw a 404. This is not only a nice-to-have but it's also the correct behaviour expected in the HTTP protocol.
It is possible to abstract this pattern out into a method so you can repeat it, or you could use Pigeon like is explained above. Alternatively, you could simply copy and paste the code for all of your resources. Our application is simple, so we've written all the routes we'll need already.
Parameters You may have spotted another problem: now we're routing based on the HTTP method, we're using new methods such as PUT and DELETE. How are we going to access our user's input through these methods?
We'll start by creating a MY_Controller.php file inside our application/ core directory. CodeIgniter will automatically load this for us so we can extend our controllers from it and have our API functionality baked in. This is very similar to the MY_Model and MY_Controller we created in volume one.
class MY_Controller extends CI_Controller {
}
Inside our MY_Controller, we're going to use the controller's constructor to parse the parameters we get from the user. We'll populate a $this->params array, so we don't have to care about the HTTP method by the time we reach the controller.
15
public $params = array();
public function __construct() { parent::__construct(); }
Right. Here, we can check the request method and get our correct parameters based on the request body. Rather than shuttle around entire JSON documents here, we'll instead make the assumption that every request body we recieve in our API is of the type application/x-www-formurlencoded - the same as if we were to submit a form.
This assumption means that the raw request body will be a parsable 'query string'. From this assumption, we can write a reasonably generic request parser. If it's a GET or POST request, great, we're done. If it's anything else, we can use the PHP function parse_str to give us what we want:
Fetch the method:
$method = strtoupper($_SERVER['REQUEST_METHOD']);
For our generic methods:
if ($method == 'GET') { $this->params = $_GET; } elseif ($method == 'POST')
16
{ $this->params = $_POST; }
…and for the rest? To get the raw request body, we can access php://input, which is a sort-of file handle we can read from:
else { parse_str(file_get_contents('php://input'), $this->params); }
…and we're done! Now when our request is routed through to the controller we'll have an array filled up with all the parameters–regardless of HTTP method–given to us. Fantastic.
Responding Now we've routed our request through to our controller, we need to start thinking about the response we send to the user.
Our API needs to respond with the data in a format that a computer can parse. There are a few popular serialisation formats used to transfer data: XML, JSON, Yaml and even Serialised PHP. My serialisation format of choice is JSON. JSON is easy to read for humans and has very quick native parsing libraries for most languages/frameworks.
Since we're working with arbitrary data, however, we can offer a variety of different output formats for platforms that might be tied in to using
17
something specific. To keep things simple for the sake of the book, we'll offer JSON and Serialised PHP (both require calling a single PHP function). It's usually a good idea to offer XML too, but this requires much more complex code, so to keep this book short and sweet we'll stick with the simpler options.
Besides, who wants to use XML?
You may have seen the user specifying the response type in the URL with an extension (resources/1.json). You may have seen it through a query string parameter (resources/1?format=json). Both are excellent ways of approaching the problem, but neither are truly compliant with the HTTP spec.
A much better aproach is to use the HTTP Accept header. The concept of responding with different types is baked inherently into HTTP with this header, and to be truly RESTful we can use the Accept header in our request to specify what format we want the resource in.
That being said, the extension format (resources/1.json) is an easier option for a lot of developers new to working with APIs. Because of this, and for the purposes of education, our API will offer both methods.
The Extension Method Ideally, we want to abstract away the response mechanism into MY_Controller so we don't have to repeat any code. We'll also detect the response type here behind the scenes before we run the method itself.
To begin with, we'll need to modify our routing a little more. In order to support extensions in the URL, we need CodeIgniter to route correct even if an extension format exists.
18
For instance, RESTful routing dictates that to request all trackers in JSON format (with the extension method) we need to send a GET request to /trackers.json. CodeIgniter would then look for a Trackers.json controller, which, naturally, doesn't exist.
Instead, we can loop through all our routes and append a small regular expression to the 'from' route. This will match and capture the format for our extensions. It's a clever bit of code:
foreach ($route as $key => $val) { $route[$key . '(\.[a-zA-Z0-9]+)?'] = $route[$key]; unset($route[$key]); }
Now, if we GET /trackers.json we will be correctly routed to trackers#index.
Inside MY_Controller.php, at line 24 or so (after the HTTP method detection), we can then find the last segment of the URI and use that to detect an extension. The code for this is simple:
$last_segment = explode('/', $this->uri->uri_string); $last_segment = $last_segment[count($last_segment) - 1];
if (strpos($last_segment, '.') > 0) { $content_type = explode('.', $last_segment); $content_type = $content_type[count($content_type) - 1]; }
19
We only want to support certain extensions. Let's set up a small array to contain the supported formats:
public $formats = array( 'json' => 'application/json', 'php' => 'application/php' );
We're storing them alongside their MIME types so that we can search for them through the Accept header later. What happens when the specified format doesn't exist in our array? We could default to JSON or something, but this isn't truly HTTPy.
Instead, we can return an HTTP 406 Not Acceptable:
if (!in_array($content_type, array_keys($this->formats))) { $this->status_code = 406; $this->respond(); }
Define the $this->status_code variable at the top of the class:
public $status_code = 200;
…and then implement the respond() method to respond with the correct statement and end the execution:
20
protected function respond() { $this->output->set_status_header($this->status_code); exit; }
What about when we specify a correct response type? Well, we'll want to set it, send out the header and let our request continue. Following on from the conditional on line 45, we'll add the else statement:
else { header('Content-Type: ' . $this->formats[$content_type]); $this->response_format = $content_type; }
…and declare $this->response_format at the top of the class:
public $response_format = FALSE;
This line uses PHP's header() method to set the Content-Type header to our response type. We then set the type internally so we can output it later. Easy.
We now need to think about the best way of passing around data from our controller method to the output mechanism. Again, we want to abstract this away as much as possible, so we don't need to write any repetitive boilerplate code.
21
Following on from the conventions set in Volume One, we'll use $this>data as a variable to store all the data we want to render out. Define it at the top:
public $data = array();
…and define the _remap() function we learnt about in Volume One, to intercept the method call:
public function _remap($method, $params = array()) { }
Our _remap() function is going to be responsible for calling the method and outputting the content in a correct format. This code is straightforward:
call_user_func_array(array($this, $method), $params);
$method = '_format_' . $this->response_format; $formatted_data = call_user_func_array(array($this, $method), array($this->data));
$this->respond($formatted_data);
This is a sensible implementation. There are a couple of gaps we'll need to fill in order to get this to work, but before we do I'm going to take a step back and look at the code we've got in our controller already. It's getting a bit unruly, so let's use this opportunity to refactor it a bit.
Move all of the HTTP verb detection code into a separate method:
22
protected function _detect_method() { $method = strtoupper($_SERVER['REQUEST_METHOD']);
if ($method == 'GET') { $this->params = $_GET; } elseif ($method == 'POST') { $this->params = $_POST; } else { parse_str(file_get_contents('php://input'), $this->params); } }
Do the same with the response type detection:
protected function _detect_response_type() { $content_type = FALSE;
if (strpos($this->uri->uri_string, '.') > 0) { $content_type = explode('.', $this->uri->uri_string); $content_type = $content_type[count($content_type) 1]; }
23
if (!in_array($content_type, array_keys($this->formats))) { $this->status_code = 406; $this->respond(); } else { header('Content-Type: ' . $this->formats[$content_type]); $this->response_format = $content_type; } }
This cleans up and separates the code out a bit more. Much better.
Let's now fill in those gaps. The first gap is our formatting functions. The _remap() method looks for a method named _format_json() and _format_php() for JSON and PHP formatting, respectively. The beauty of this approach is that you can easily extend it to add _format_xml() or _format_jsonp() or whatever. You just add a new method.
Let's implement both those functions:
protected function _format_json($object) { return json_encode($object); } protected function _format_php($object) { return serialize($object); }
Simple, 'aint it?
24
The final gap to plug is the respond() method. respond() currently only works by setting the header and exiting. Add an arbitrary response string and echo it out:
protected function respond($data = '') { $this->output->set_status_header($this->status_code); echo $data;
exit; }
We're now done implementing the extension method. The majority of the responding logic is now implemented; extending our API to support the recommended Accept header will be a piece of cake.
HTTP Accept Header Before we write any more code, let's look at the HTTP spec:
The Accept request-header field can be used to specify certain media types which are acceptable for the response
In the context of resources, this means we can use the Accept header to specify the content type of the representation of that resource.
We're looking to respond with JSON and PHP. As we wrote above, these media types are application/json and application/php. We've stored these in our $this->formats array, so fetching them is going to be easy.
25
At line 67, after the strpos conditional, add an elseif clause:
elseif ($key = array_search($_SERVER['HTTP_ACCEPT'], $this->formats)) { $content_type = $key; }
…and that's all it takes!
We can test all this using cURL. No content type:
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/trackers HTTP/1.1 406 Not Acceptable
…the extension method:
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/trackers.json HTTP/1.1 200 OK Content-Type: application/json
…and the header method:
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/trackers -H 'Accept: application/php'
26
HTTP/1.1 200 OK Content-Type: application/php
Perfect.
We've now got enough code to build a small, simple RESTful API. We've got a clever RESTful routing system that supports multiple HTTP verbs. We support multiple response types and we have a mechanism with which we can spit out data to our client in the response type they specify.
Versioning So, we've built a basic API and everything's going great. We can document our API and roll it out to the public, or keep it internally and interconnect our systems.
Until that day arrives. The client turns up in her Mercedes ML350, an eager look on her face. The API has been extremely active, she says, and it's boosting sales. They're thrilled, but her and the team have decided to add a bunch of new features.
“Great!” you say, and you begin to sketch out the ideas on the paper in front of you. However, soon you begin to realise that things are more problematic than they seem. Some of the changes that the client has requested will modify existing functionality.
If you change the API, all the existing applications using it will break!
Thankfully, there's a solution. Let me introduce you to versioning.
Versioning is the process of separating different versions of the API and running them simultaneously. By allowing multiple versions of the API to
27
operate, we can keep older applications running smoothly and introduce new features and changes to newer applications.
It also means that we can alert application developers that we plan on deprecating their version of the API and give them time to upgrade. To top it off, it also means we can release some new features and let the developers at the cutting edge try it in BETA before we make a stable release.
Versioning is a complex problem with many different approaches. The solution below is just one of the ways I've built APIs in the past.
We'll begin by separating out our API code into a version-specific directory. Create an application/controllers/v1 directory and move the controllers into it.
Our next step is to create a base-level API_Router controller. We'll modify our routes to process all of the requests through the API router. The API router will then check for the version and load up the correct controller in the correct directory appropriately.
In our routes, we need to make a couple of changes. First, we'll update all the backreferences (the $1s and $2s in the routes) and add one. $1 becomes $2 and $2 becomes $3.
Next, we'll modify line 68:
$route[$key . '(\.[a-zA-Z0-9]+)?'] = $route[$key];
We'll insert a little bit before to capture the version number. Like with our response formats, we'll offer two methods of specifying the API version: URL and HTTP Header. Like before, we'll begin with the URL.
28
We'll use another regex–this time before the route, hence updating the backreferences–to capture the version number and redirect all traffic through our API_Router controller. Line 68 will now look like so:
$route['(v[0-9]+\/)?' . $key . '(\.[a-zA-Z0-9]+)?'] = 'api_router/index/$1/' . $route[$key];
Now create an application/controllers/api_router.php controller file with an API_Router class that inherits from CI_Controller:
class API_Router extends CI_Controller {
}
We're routing everything through index, so let's define that:
public function index()
Due to the way our regex works, we'll need to check to see quite where the relavent parts of our request are. You'll see that if we have a v1/ directory the extra / is interpreted by CodeIgniter's router as a separate segment. So, we need to offset the routes by one if we have a version in the URL.
Thankfully, this is easy:
29
$args = func_get_args(); $version = $args[0]; $i = ($version) ? 1 : 0;
$controller = $args[$i + 1]; $action = $args[$i + 2];
We'll change the router so that the directory, class and method are rerouted. It's important for us to do as much work as we can to change the state and 'fool' our other controllers into thinking it's being called directly. This way we don't have to do anything special in our controllers, and any other developers will have less to think about.
$this->router->directory = $version . '/'; $this->router->class = $controller; $this->router->method = $action;
We can grab our parameters from the rsegments array. If we don't have any, we'll get an empty array:
$parameters = array_slice($this->uri->rsegments, $i + 5);
We'll also use this opportunity to strip out the extension (if it exists) from the last parameter. This will mean GET /trackers/test.json will route to Trackers#show with test as a parameter, rather than test.json.
30
if ($parameters && strpos($parameters[count($parameters) 1], '.')) { $arr = explode('.', $parameters[count($parameters) - 1], 2); $parameters[count($parameters) - 1] = $arr[0]; }
We then want to grab the controller, load it and instantiate it. Remember to search for it in the version directory!
require_once APPPATH . 'controllers/' . $version . '/' . $controller . '.php'; $controller = new $controller();
Next up, to simulate CodeIgniter, we'll check for a _remap() method. If it's there, call it:
if (method_exists($controller, '_remap')) { $controller->_remap($action, $parameters); }
If it doesn't exist, call the method or throw a 404:
else { if (method_exists($controller, $action))
31
{ call_user_func_array(array($controller, $action), $parameters); } else { show_404(); } }
And that's it!
Let's test it out:
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/v1/trackers.json HTTP/1.1 200 OK
[{"id":"ebook_downloads","name":"eBook Downloads"},{"id":"website_visits","name":"Visits"}]
…and for the show method?
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/v1/trackers/website_visits.json HTTP/1.1 200 OK
{"id":"website_visits","name":"Visits"}
32
Fantastic!
If we now want to upgrade our API, we can create a new v2 directory, copy the files over, make our changes and be done.
HTTP Accept… again Like before, there's a slightly more complicated, but much more HTTP compliant way of handling versioning. Let's take a step back and think about it conceptually.
When we're sending the Accept header, we're telling the server what forms of response are acceptable. By response, we're really referring to a representation of the resource.
So, when we request something from the server, we're requesting some form of representation of that resource. When we make a request to one version of the API, it's exactly the same resource as another version–it's the same data, the same row in the database, right?–it's just a different representation. Geddit?
We can then put two and two together; this means we can use the Accept header to request a different version, a different representation of the resource!
How does this work? Accept header mime types can accept parameters of their own. You might have seen this when sending a header for ContentDisposition: attachment; filename=blah.pdf. We can do the same thing with Accept:
Accept: application/json; version=v1
33
Sweet. Let's implement that.
Firstly, we'll need to slightly change our response type detection logic. Right now we're matching the entire mime type. When we add a version parameter, it's going to break that. So, open up MY_Controller.php and change line 68:
else { $header_type = explode(';', $_SERVER['HTTP_ACCEPT']); $header_type = $header_type[0];
if ($key = array_search($header_type, $this->formats)) { $content_type = $key; } }
Next, we want to check for the header in API_Router.
At line 20:
if (!$version && strpos($_SERVER['HTTP_ACCEPT'], 'version=') !== FALSE) { $arr = explode(';', $_SERVER['HTTP_ACCEPT']); $arr = explode('=', trim($arr[1])); $version = $arr[1]; }
34
It's probably a good idea to check to see if we have a valid version, rather than let it complain loudly and ejaculate error everywhere. Overwrite the require_once statement:
$controller_path = APPPATH . 'controllers/' . $version . '/' . $controller . '.php';
if (file_exists($controller_path)) { require_once $controller_path; $controller = new $controller(); } else { header('HTTP/1.1 406 Not Acceptable'); header('Status: 406 Not Acceptable');
exit; }
If we don't have a valid version, we'll respond, again, with a 406 Not Acceptable. If we do have a valid version, we'll continue on with the request and load the controller.
Let's test it:
$ curl -i http://localhost/efendibooks/ codeigniter-handbook-vol-2/index.php/trackers -H "Accept: application/json; version=v1" HTTP/1.1 200 OK
35
[{"id":"ebook_downloads","name":"eBook Downloads"},{"id":"website_visits","name":"Visits"}]
It works perfectly!
Error Handling The last piece of our API puzzle is handling errors. So far, we've managed to build a system that responds by setting the status code and echoing out the data. That's great, but when our API starts to get a bit bigger, we'll begin to start duplicating code.
What we really want is a flexible way of notifying our core API code that something's gone wrong. This could be a 404 error, where a resource can't be found, it could be an issue with the user's input or it could be a permission issue. Hell, it could even be an internal problem we want to report and/or log.
Whatever the case is, we want one centralised place to handle these errors.
You guessed it, we're going to use exceptions.
Custom Exceptions We're going to create a bunch of custom exceptions. Each exception will throw a base exception which will, in turn, respond with the correct status code and the exception information.
In order to separate our code out and ensure interoperability, we're also going to use namespaces. If you've not used namespaces before, I'd recommend giving the docs a read to get up to speed with them. I'll do my
36
best to explain what's going on, but a deeper understanding of the benefits and gotchas of namespaces is always useful.
Create a new library directory, application/libraries/api and a new file, Exceptions.php. First, we're going to define a small Exceptions class which will be loaded by CodeIgniter. It won't do anything, it's just so that we can add our API exceptions file to the $autoload array without it crying.
This needs to be in the global namespace in order for CodeIgniter to load it properly. We can use the curly brace namespace syntax to define it within the global namespace:
namespace { class Exceptions { } }
Let's go ahead and autoload that:
$autoload['libraries'] = array('api/exceptions');
We're now going to set up our base exception. It's a very simple class inside a new namespace, API\Exceptions:
namespace API\Exceptions { class Exception extends Exception { }
37
PHP's exceptions, by default, contain a code/number. We can use this code when we spit the error out to the user as the HTTP status code.
We can now set up another exception to handle the 404 Not Found error case:
class Resource_Not_Found extends Exception { public function __construct($resource) { parent::__construct(404, "The $resource you tried to retrieve can't be found"); } }
Our application is very simple, so very little can go wrong. Other than PHP and database errors, which we don't want to display to the user anyway (we should be logging them and hiding them in the background), there's very little else for us to throw for now.
We don't want to duplicate any code, so we'll place the throw call in our model. In this example I'm using my MY_Model, similar to the one we built in volume one. Override the get() method. If you're not using MY_Model, you could put this in another custom model method (or, not ideally, in your controller).
public function get($id) { $row = parent::get($id);
38
if (empty($row)) { throw new API\Exceptions\Resource_Not_Found('tracker'); }
return $row; }
Excellent. Now, if a tracker doesn't exist and we try to fetch it, the API\Exceptions\Resource_Not_Found exception will be thrown.
There's one piece to the puzzle that we're missing.
Currently, when our exception is thrown, it's spewed out to the screen as text / HTML. Instead of doing that, let's make it return as a more useful format with the associated HTTP status code.
Inside MY_Controller.php, we're going to wrap the entire _remap() method in a try/catch block:
try { call_user_func_array(array($this, $method), $params);
$method = '_format_' . $this->response_format; $formatted_data = call_user_func_array(array($this, $method), array($this->data));
$this->respond($formatted_data); }
39
catch (API\Exceptions\Exception $e) {
}
Inside the catch block, we want to grab the exception and output it in friendly JSON or PHP.
$method = '_format_' . $this->response_format; $formatted_data = call_user_func_array(array($this, $method), array( 'error' => TRUE, 'type' => join('', array_slice(explode('\', get_class($e)), -1)), 'message' => $e->getMessage() ));
$this->status_code = $e->getCode(); $this->respond($formatted_data);
We can set the status code, build up a sensible response array and output it. We're using a combination of join() and get_class() to grab the class name and get rid of the namespace.
Now, if we make a request to an invalid tracker:
$ curl -i http://localhost/codeigniter-handbook-vol-2/v1/ index.php/trackers/some_nonexistant_tracker.json HTTP/1.1 404 Not Found
40
{"error":true,"type":"Resource_Not_Found_Exception","message":"The tracker you tried to retrieve can't be found"}
And there we have it!
The implementation I've shown you is very simple, but very flexible. Now, we can throw any kinds of exception from anywhere within the controllermodel stack and they will be caught by the try/catch.
Exceptions are a brilliant way of triggering errors in your application. Where you can, throw an exception. Because it's abstracted out, it means you can change the output format and add metadata if you need too.
Additionally, exceptions don't care about scope. They bubble up through the stack trace until they hit the try/catch block. This means you can throw them from your controller, model or a third-party library.
Using a combination of exceptions and HTTP status codes is a great, extensible and HTTP compliant way of handling errors in your application. In the rest of this book we'll implement more exceptions as our API gets more complex.
41
…in which we examine various authentication methods, secure our application and provide a mechanism to ensure that requests are coming from the people with access and are blocked from people without it. We’ll examine API keys and shared secrets, request signing and why it’s so, so important to use SSL.
43
How We Authenticate When dealing with secured, sensitive and/or proprietary data, it's imperative to lock your application up and only let in the few who need access.
Like many other topics we've discussed, there are many different solutions to the problem of authenticating your API users. Just a quick Google will unveil a myriad of alternatives. Again, the solution below is just one of the many possible options.
HTTPS Before we go any further, in order for any API to be secure, all your data has to be going through HTTPS. Using an SSL certificate means that any data is encrypted before it goes down the wire. That means that nobody can tamper or view the data you send back and forth except you and the server.
When dealing with any confidential information, especially authentication credentials, do yourself a huge favour and use HTTPS. Please.
Access Tokens and Request Signatures The way we're going to authenticate our API users is through access tokens for identification and request signatures for securing and authenticating the request.
How does this work?
When the user sends a request to us, they'll include two custom headers: the access token and the request signature. The access token is used to identify the user within the database.
Once we know who the user is, we need to make sure it's really them. We do this with a signature. A signature is a hash that uniquely identifies that
44
request to that user. We're going to do this by grabbing the requested URL and parameters, combining it with a shared secret and hashing it, along with a timestamp.
By combining and hashing these elements, we can make it incredibly difficult for anybody to get access to our API unless they deserve it.
Writing the code Authenticating and securing our API is actually quite easy. We'll begin by checking for the headers. In MY_Controller.php at line 26 (inside the try block):
$this->authenticate();
Let's write that function. First, let's check for the existence of the headers. The HTTP headers we're going to use are custom, so they need to be prefixed with X-. We'll check for these:
X-Access-Token: blah X-Request-Signature: blah X-Request-Timestamp: blah
We'll grab the headers:
$token = @$_SERVER['HTTP_X_ACCESS_TOKEN']; $signature = @$_SERVER['HTTP_X_REQUEST_SIGNATURE']; $timestamp = @$_SERVER['HTTP_X_REQUEST_TIMESTAMP'];
45
Next, we check the access token against a database table. Create a new table:
CREATE TABLE `api_clients` ( `access_token` varchar(40), `shared_secret` varchar(40) ) CHARSET=utf8;
…and perform the check:
$token = $this->access_token->get_by('access_token', $token);
if (!$token) { throw new API\Exceptions\Authentication(); }
If we get a correct token, we want to build up our own signature and compare it against the signature in the request.
We build the signature by taking the request path, the value of $_GET (which we'll serialise in a query string) and the raw request body. We then add the timestamp to it and the shared secret, hash it all, and tada!
else { $hash = $this->uri->uri_string . http_build_query($_GET) . http_build_query($this->params); $hash .= $timestamp . $token->shared_secret;
46
$hash = sha1($hash);
if ($signature !== $hash) { throw new API\Exceptions\Authentication_Signature(); } }
We're throwing a new exception here because it's a different problem. The Authentication_Exception is thrown when we can't identify the user. This will cause a 401 Unauthorized response.
When our signature is incorrect, this is because we've recieved a botched request. We can identify the client fine, the problem is with the request itself. This will throw a 400 Bad Request.
Let's implement those exceptions. Open up our libraries/ API_Exceptions.php file:
class Authentication extends Exception { public function __construct() { parent::__construct("Your access token is either missing or incorrect. Please check the X-Access-Token header and try again.", 401); } }
class Authentication_Signature extends Exception {
47
public function __construct() { parent::__construct("There was a problem with the request signature you supplied.", 400); } }
We're responding with some useful messages to help debugging and erroring out with a correct status code.
Let's test our authentication. Create a new client:
INSERT INTO `api_clients` (`access_token`, `shared_secret`) VALUES ('4395dd07a3cfe84d9655bb2542907f3acd0024fe', '3c697e1314808f56bd44bc5ccb4765607b433715');
Without an access token:
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/v1/trackers/website_visits.json HTTP/1.1 401 Unauthorized
{"error":true,"type":"Authentication_Exception","message":"Your access token is either missing or incorrect. Please check the X-Access-Token header and try again."}
With an incorrect access token:
48
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/v1/trackers/website_visits.json
-H
'X-Access-Token: blah' HTTP/1.1 401 Unauthorized
{"error":true,"type":"Authentication_Exception","message":"Your access token is either missing or incorrect. Please check the X-Access-Token header and try again."}
With a correct access token, but no signature:
$ curl -i http://localhost/codeigniter-handbook-vol-2/ index.php/v1/trackers/website_visits.json -H 'X-Access-Token: 4395dd07a3cfe84d9655bb2542907f3acd0024fe' HTTP/1.1 400 Bad Request
{"error":true,"type":"Authentication_Signature_Exception","message":"There was a problem with the request signature you supplied."}
…and with a correct signature:
$ curl -i http://localhost/efendibooks/ codeigniter-handbook-vol-2/index.php/v1/trackers/ website_visits.json -H 'X-Access-Token: 4395dd07a3cfe84d9655bb2542907f3acd0024fe' -H 'X-Request-Signature: 35ef61fde0b918065fce55b5fcb219fbdfc6cded' -H 'X-Request-Timestamp: 1345030628' HTTP/1.1 200 OK
49
{"id":"website_visits","name":"Visits"}
Fantastic!
Like I said above, this is just one of the ways of securing your application. There are hundreds of different protocols and techniques.
You could use a username and password combination through the HTTP basic authentication and WWW-Authenticate header. You could use a secure request signing and client identification system like oAuth. You could use an XML-based enterprise system like SAML.
I hope that I've demonstrated a great technique for authenticating users and securing your API. Remember to always use HTTPS, and spend some time testing and ensuring that there are no obvious gaps.
Throttling One of the easiest techniques to reducing sever load and ensuring your API remains performant is throttling. Throttling is deliberately limiting each user (each client) to a certain number of requests.
Limiting the user to 100 requests per hour, or 1000 requests per day, ensures that the system isn't overloaded with requests and clients don't abuse the API. It's very easy to write a script that makes constant requests to an API and overloads it with traffic. Adding a throttle (rate limit) to your API is one easy way of preventing it.
Let's add some columns to our api_clients table:
50
ALTER TABLE `api_clients` ADD `throttle_count` INT DEFAULT NULL
NULL
AFTER `shared_secret`;
ALTER TABLE `api_clients` ADD `throttled_at` DATETIME
NULL
AFTER `throttle_count`;
These columns will track the client's remaining number of requests and the last time the throttle began, respectively.
We're going to throttle the API at the _remap() stage, much like authentication. So, after $this->authenticate():
$this->throttle();
Let's also adjust authenticate a little. At line 117:
$this->client = $this->api_client->get_by('access_token', $token);
if (!$this->client)
…and also replace the other instance of $token:
$hash .= $timestamp . $this->client->shared_secret;
We'll also define $client:
51
protected $client = FALSE;
Now let's write the throttle() method. First, let's check to see if the throttle rate for this client is greater than our limit. We're going to define our limits as constants at the top of the class, like so:
const LIMIT = 100; const LIMIT_TIME = 3600;
Using constants means we can easily configure the values without having to mess around digging through code. We're setting LIMIT_TIME to 3600, the number of seconds in an hour.
First, check the throttled at time. If it's more recently than an hour, we want to update the database:
if (strtotime($this->client->throttled_at) < time() self::LIMIT_TIME) { $this->client->throttle_count = 1; $this->api_client->update_by('access_token', $this->client->access_token, array( 'throttle_count' => 1 )); }
Then, check if we're throttled. If we're throttled, throw an exception:
52
if ($this->client->throttle_count > self::LIMIT) { throw new API\Exceptions\Throttled(); }
If we're not throttled, upgrade the throttle count:
else { $this->api_client->update_by('access_token', $this->client->access_token, array( 'throttle_count' => $this->client->throttle_count + 1 )); }
We're duplicating some code here, so let's refactor a little. After necessary refactoring, the throttle() method looks like this:
protected function throttle() { $throttle_count = FALSE; $throttled_at = $this->client->throttled_at;
if (strtotime($this->client->throttled_at) < time() self::LIMIT_TIME) { $throttle_count = 1; $this->client->throttle_count = 1; $throttled_at = date('Y-m-d H:i:s');
53
}
if ($this->client->throttle_count > self::LIMIT) { throw new Throttled_Exception(); } else { $throttle_count = $this->client->throttle_count + 1; }
if ($throttle_count !== FALSE) { $this->api_client->update_by('access_token', $this->client->access_token, array( 'throttle_count' => $throttle_count, 'throttled_at' => $throttled_at )); } }
We can now create the API\Exceptions\Throttled exception. In libraries/ API_Exceptions.php:
class Throttled extends Exception { public function __construct($date) { parent::__construct("The rate limits for this access_token have been exceeded. Please try again at " . $date, 400);
54
} }
We pass through a date to the constructor to display in the error message; to be extra helpful we'll even tell the client when their rate limit is reset.
Now, modify the call to throw in throttle():
$date = strtotime($this->client->throttled_at) + self::LIMIT_TIME; $date = date('Y-m-d H:i:s', $date);
throw new API\Exceptions\Throttled($date);
..and it works smoothly and seamlessly.
It's a simple technique, but it's very easy to do, so if you're finding that your clients are using too much bandwidth or are constantly hitting the server and it's causing your system problems, throttling is a great solution.
55
…in which we discover brilliant new ways of debugging our API, take a look at declaratively describing our API in a config file and help other developers use our API easier with the OPTIONS header and an intelligent debug mode. We’ll also build a small testing script using PHPUnit to ensure our API behaves as we expect it to.
57
OPTIONS There's one HTTP verb we've been ignoring. OPTIONS is arguably one of the most useful and most underused verbs in the entire HTTP lexicon.
OPTIONS is described in the HTTP spec as:
This method allows the client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.
What this means is OPTIONS gives developers using our API a secret hidden lifeline. It gives them a comprehensive description of the API they can access directly from cURL.
A well-built response to an OPTIONS request will let you cut down on documentation and will make it much easier for developers consuming the API to automate requests and build clients.
However. How on Earth do we go about adding OPTIONS support?
We certainly don't want to create a tonne of new routes. We don't want to add anything to our existing methods. We'd rather not add extrenuous code to all our controllers.
Instead, what we can do is document the API's supported methods in a separate file. A manifest document, if you will. A big long array of everything that the API supports and can do. We can then expose that for each endpoint.
58
As the HTTP spec doesn't define what the response body should look like, we'll have to just respond with a sensible, consistent format and hope that that's enough.
Our response is going to look like this:
{ "GET": { "description": "Retrieve all trackers", "example": [ { "id": "website_visitors", "name": "Website Visitors" }, { "id": "ebook_downloads", "name": "eBook Downloads" } ] }, "POST": { "description": "Create a new tracker", "parameters": { "id": { "type": "string", "description": "An alphanumeric + underscored identifier" }, "name": { "type": "string", "description": "A display name" }, }, "example": { "id": "some_tracker_id", "name": "A Special Tracker"
59
} }, }
See how useful that is? Let's begin implementing it.
We're going to describe our API in a config file. The API manifest will be a big structure of endpoint information, just like the JSON object above. We'll describe each endpoint in detail and then respond with the information when a user OPTIONS requests it.
Create two new files, config/api_manifest.php and libraries/api/ manifest.php. We're going to open up a new namespace where we'll store our manifest classes.
Rather than holding all the manifest information in one single associative array, we're going to build up the data structure with objects. This is a much more flexible and extensible approach. To achieve this, let's define a few new classes. In libraries/api/manifest.php:
namespace API\Manifest { class Endpoint { } class HTTP_Verb { } }
The classes don't do anything, no. But that's not the point. The fact that they are classes means we can extend this system to do pretty much anything.
60
There's one more class which will have a little bit more code in it:
class Parameter { public $type; public $description;
public function __construct($type, $description) { $this->type = $type; $this->description = $description; } }
We could automatically validate the existence and type of parameters by adding it to the Parameter class. We could load models dynamically by associating each endpoint (and thus its children) to a model. We could even generate scaffolding code from a manifest.
Let's autoload that library:
$autoload['libraries'] = array( 'api/exceptions', 'api/ manifest' );
…but, of course, this will fail as we're defining the various classes inside a namespace. We'll use the global namespace trick like before:
namespace {
61
class Manifest { } }
Inside config/api_manifest.php, things are going to get a little more complex. We're going to begin by creating a new array for our manifest:
namespace API\Manifest;
$manifest = array();
Next, we'll create a new endpoint:
$manifest['trackers'] = new Endpoint();
We can now define the GET request for that endpoint:
$manifest['trackers']->GET = new HTTP_Verb();
Let's give it a description:
$manifest['trackers']->GET->description = "Retrieve all trackers";
…and set our example as an array:
62
$manifest['trackers']->GET->example = array( (object)array( 'id' => "website_visitors", 'name' => "Website Visitors" ), (object)array( 'id' => "ebook_downloads", 'name' => "eBook Downloads" ), );
We can do the same for POST:
$manifest['trackers']->POST = new HTTP_Verb(); $manifest['trackers']->POST->description = "Create a new tracker"; $manifest['trackers']->POST->example = array( 'id' => "some_tracker_id", 'name' => "A Special Tracker" );
…as well as use our Parameter class to set up a parameter list:
$manifest['trackers']->POST->parameters = array( 'id' => new Parameter('string', 'An alphanumeric + underscored identifier'), 'name' => new Parameter('string', 'A display name') );
Let's also do the same for trackers/something. These will be matched in the order they're declared in, so make sure you put more specific routes, like the following, above the more generic routes, like trackers:
63
$manifest['trackers/(.*)'] = new Endpoint(); $manifest['trackers/(.*)']->GET = new HTTP_Verb(); $manifest['trackers/(.*)']->GET->description = "Retrieve a specific tracker"; $manifest['trackers/(.*)']->GET->parameters = array( new Parameter('string', 'The tracker identifier') ); $manifest['trackers/(.*)']->GET->example = (object)array( 'id' => "website_visitors", 'name' => "Website Visitors" );
We'll repeat this process until we've documented the entire API. I'll omit this from the book as not to waste a lot of your time.
At the end of the file, return the $manifest variable. You can return variables from a file in PHP and access them in the include, which we'll see in a moment:
return $manifest;
Now we've got our API described in the config/api_manifest.php file, we can integrate it with the OPTIONS header.
Our routes are being set based on the current HTTP request. This means that if we make an OPTIONS request to the API, we will be routed straight through and we will encounter an error.
Let's add another function to our _remap() try/catch block, around line 34 (after $this->throttle()):
$this->options();
64
Define that method:
protected function options()
If the user has made an OPTIONS request, we'd like to load up our manifest file, loop through the endpoints, run a regex match against the endpoint URLs and output the endpoint info! Simple, right?
Check for the OPTIONS:
if (strtoupper($_SERVER['HTTP_METHOD']) == 'OPTIONS') {
Load the manifest file. Because we're returning the $manifest variable from the file, we can now assign it in the include:
$manifest = include(APPPATH . 'config/api_manifest.php');
Let's now loop through the manifest's endpoints:
foreach ($manifest as $route => $endpoint) {
At this point we want to run a preg_match on the URL, matching it to the endpoint URL. If there is a match, we want to grab the info and output it:
65
if (preg_match("#" . $route . "#", $this->uri->uri_string)) {
Use our trusty respond() method to spit out the options info:
$method = '_format_' . $this->response_format; $formatted_data = call_user_func_array(array($this, $method), array( $endpoint ));
$this->status_code = 200; $this->respond($formatted_data);
This is great, but we're now starting to duplicate code. Let's move the call to the formatting into its own format() method:
protected function format($data) { $method = '_format_' . $this->response_format;
return call_user_func_array(array($this, $method), array( $data )); }
Swap out the calls in our controller. Inside the main try block:
$formatted_data = $this->format($this->data);
66
…and in the catch:
$formatted_data = $this->format(array( 'error' => TRUE, 'type' => join('', array_slice(explode('\', get_class($e)), -1)), 'message' => $e->getMessage() ));
…and finally inside options():
$formatted_data = $this->format($endpoint);
…and there you have it!
You may want to turn off authentication for OPTIONS requests, as they should be really easy to access. Doing this is simple; move the call to $this->options() above the call to $this->authenticate().
We've now built a sleek way of providing our developer community with great documentation in-line. This implementation can be extended easily to support HTML and give you brilliant, viewable documentation. The information is parsable as JSON, which can be used for code generation.
OPTIONS is a really cool feature of HTTP and I highly recommend supporting it whenever you're building a public API.
67
Debug mode There's one piece of the API puzzle that we're missing. Currently, to find out essential information about our requests we're having to die(var_dump()) information and hack the system a bit to bypass things like authentication. This makes debugging a pain.
Instead, it would be much better if we could access this information as part of our API response. A debug mode, which, when enabled, would spit out crucial information about the request and the server.
Sounds, good, doesn't it?
First of all, how are we going to enable this debug mode? We certainly don't want to expose it to everyone. Selectively enabling it can be done in a number of ways.
It can be done using an environment; if you're on a staging / development server you may want to permanently enable debug mode, if you're on production, you'll want to disable it. It can be done with a boolean attached to a specific user (enable for admins or mark certain users as core developers?) Whatever works for you.
We're going to enable debugging based on environment. We'll be using CodeIgniter's environment constant to determine if we're in the development environment. If we are, we'll enable debug mode.
Inside MY_Controller.php, at line 192 (inside format()), we'll check for the development environment:
protected function format($data) { if (ENVIRONMENT == 'development')
68
{ $data = $this->add_debugging_info($data); }
Our output with debugging enabled will be:
{ "_debug": { /* debugging information */ }, "result": /* request result */ }
Define the method:
protected function add_debugging_info($data) {
What information are we going to display?
We'll want the basics of the user's request. Their IP, what path they've visited, what controller we've been routed to and the access token they've submitted:
$debugging_info = array( 'request' => array( 'ip_address'
=> $this->input->ip_address(),
'uri_string'
=> $this->uri->uri_string,
'controller'
=> $this->router->class,
'method'
=> $this->router->method,
69
'access_token' => @$_SERVER['HTTP_X_ACCESS_TOKEN'], 'user_agent'
=> $_SERVER['HTTP_USER_AGENT']
),
We also want to show some information about the execution time and memory so we can keep track of how our application is performing. Some info on SQL queries would be useful too:
'server' => array( 'elapsed_time' => $this->benchmark->elapsed_time('total_execution_time_start', 'total_execution_time_end'), 'memory_usage' => round(memory_get_usage()/1024/1024, 2).'MB', 'total_queries' => $this->db->total_queries() ) );
This all all really useful information. There's one more technique I use that I'd really like to share.
You may have noticed that throughout these examples I've been using cURL to test my API. Before, when the API was very simple, this wasn't a problem. However, now, with authentication and headers to input, this can require a bit of work.
Wouldn't it be great if we could supply the user with an appropriate cURL command to use to test the current URL? That way, debugging information can be extracted from any client and tested instantly on the command line.
70
Let's create a new method for this:
$debugging_info['curl_guess'] = $this->_guess_curl_command();
This method will need to do a few things. It'll need to figure out what HTTP method and URL we're trying to visit. It'll need to extract all the parameters made and calculate a method signature. It'll then have to turn all this information into a runnable cURL command.
First of all, let's start the command:
$cmd = 'curl -X ' . strtoupper($_SERVER['REQUEST_METHOD']);
We're using the -X flag to specify the HTTP method. Now we want to set the content type and the version. Let's do that with the Accept header:
$cmd .= " -H 'Accept: " . $this->formats[$this->response_format] . "; version=" . API_VERSION . "'";
Where's this API_VERSION constant coming from? Let's set it in controllers/ api_router.php at line 47, just before the call to method_exists:
define('API_VERSION', $version);
71
We now want to grab the URL. This one is a bit more tricky. We want to remove the version and content type from the URL if it exists (because we're using the headers). Luckily, we can do this with a regex:
$path = preg_replace('/^(v[0-9]+\/)?([a-zA-Z0-9\/ _\-]+)(\.[a-zA-Z0-9]+)?$/', '$2', $this->uri->uri_string);
We're grabbing the version (if it exists), the path and then the extension (if it exists). From those matches we're only grabbing the path and returning that. This gives us the version and extension free path!
We then want to add the actual URL. Autoload the URL helper, and then call site_url():
$url = site_url($path);
This gives us the full URL to this request.
We also want to add any GET parameters:
$url .= http_build_query($_GET);
Now we've got the path, let's generate our authentication parameters. We know our access token–it's only going to be me using this developer account, but you could easily pull this from the database–so it's simply a case of calculating the signature:
72
$timestamp = time();
$hash = $path . http_build_query($_GET) . http_build_query($this->params); $hash .= $timestamp . $this->client->shared_secret; $hash = sha1($hash);
…but now we're duplicating code. Let's move it into a _calculate_signature() method:
$signature = $this->_calculate_signature($path, $timestamp);
…and the method definition:
protected function _calculate_signature($path, $timestamp) { $hash = $path . http_build_query($_GET) . http_build_query($this->params); $hash .= $timestamp . $this->client->shared_secret;
return sha1($hash); }
…also remember to replace the hashing up in authenticate(). Now, let's add the headers:
$cmd .= " -H 'X-Access-Token: " . $this->client->access_token . "'";
73
$cmd .= " -H 'X-Request-Timestamp: " . $timestamp . "'"; $cmd .= " -H 'X-Request-Signature: " . $signature . "'";
Penultimately, if the user has made a non-GET request, we want to pass along the data. Let's make sure we do that:
if ($input = file_get_contents('php://input')) { $cmd .= ' -d "' . urlencode($input) . '"'; }
And, last but not least, we need to append the URL itself!
$cmd .= ' ' . $url;
..and we're done!
return $cmd;
Let's head back to our add_debugging_info() method and tie it all together. Now we've accumulated all the debugging information, we can return it alongside the original data:
return array( '_debug' => $debugging_info,
74
'result' => $data );
Let's test this out:
$ curl -X GET -H "Accept: application/json; version=v1" -H "X-Access-Token: 4395dd07a3cfe84d9655bb2542907f3acd0024fe" -H "X-Request-Timestamp: 1345554192" -H "X-Request-Signature: af42bf6f0d682b1bfbc87b467fd3f5505a8df4a3" http://localhost/ codeigniter-handbook-vol-2/index.php/trackers/website_visits | python -mjson.tool { "_debug": { "curl_guess": "curl -X GET -H 'Accept: application/ json; version=v1' -H 'X-Access-Token: 4395dd07a3cfe84d9655bb2542907f3acd0024fe' -H 'X-Request-Timestamp: 1345554527' -H 'X-Request-Signature: 0441f058dd0ba972abeb70245866a00151f7ee99' http://localhost/ codeigniter-handbook-vol-2/index.php/trackers/ website_visits", "request": { "access_token": "4395dd07a3cfe84d9655bb2542907f3acd0024fe", "controller": "trackers", "ip_address": "127.0.0.1", "method": "show", "uri_string": "trackers/website_visits", "user_agent": "curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5"
75
}, "server": { "elapsed_time": "0.0377", "memory_usage": "1.85MB", "total_queries": 3 } }, "result": { "id": "website_visits", "name": "Visits" } }
I'm piping the result through python -mjson.tool, which is a great and easy way of pretty-printing JSON when you have Python installed.
You can see we've got the _debug key with all that fantastically useful debugging information, as well as the request's result. If we change the environment to something that isn't development, all of this is removed and we're given the result straight up instead.
Providing useful debugging information is imperative for you and/or other developers to be able to build and work with your API effectively and quickly. This is a great way of returning that information to them immediately.
Automated Testing Now we've created a robust, secure and debuggable API, we want to ensure that the system we build works. What happens if we make a change to some of the code and it affects another method without us realising?
76
Situations like this are perfect examples of why automated testing can be an incredibly helpful tool.
Now, let me be clear, what we're about to do isn't unit testing. Unit testing involves programatically testing a specific unit of code, like a function or a method on a class. We're about to write some functional tests, which is testing the behaviour the entire application.
We're going to use the popular PHP testing library PHPUnit 3.6.12. It's easy to install with PEAR; I'll assume from henceforth that you've successfully installed it.
Create a new folder at the root of your API, tests. Inside this folder we'll create a new file to store the tests for our tracker functionality, tests/ trackers_test.php.
Inside this file define a new class extending from PHPUnit_Framework_TestCase:
class Trackers_test extends PHPUnit_Framework_TestCase { }
Let's write a new test case that will test the tracker list functionality (GET /trackers):
public function test_get_trackers() { $tracker = (object)array( 'id' => 'website_visits', 'name' => 'Website Visits' ); $request = $this->request('GET', 'trackers');
77
$this->assertEquals(200, $request['info']['http_code']); $this->assertInternalType('array', $request['body']->result); $this->assertTrue(in_array($tracker, $request['body']->result)); }
This method begins by making a request to the API. We'll implement that in a moment. It then calls PHPUnit's assertEquals() to ensure that the HTTP status code is 200. Next, it asserts that the type of the request body's result–remember, we're still in development, so we've got to account for our debugging info!–is an array and that it contains our website_visits tracker.
Simple, right? Let's define the request() method:
protected function request($method, $path, $params = array())
Open up a cURL request and setup the options:
$curl = curl_init($this->base_url . $path);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($curl, CURLOPT_POSTFIELDS, $params); curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
Now let's put together the headers. First, the Accept and User-Agent header:
78
$headers = array();
$headers[] = 'User-Agent: CodeIgniter Handbook Vol. 2 API Unit Tests'; $headers[] = 'Accept: application/json; version=v1';
Next, the access token:
$headers[] = 'X-Access-Token: ' . $this->access_token;
Now generate the signature:
$timestamp = time();
$hash = $path . http_build_query($params); $hash .= $timestamp . $this->shared_secret;
$signature = sha1($hash);
…and add it to the headers:
$headers[] = 'X-Request-Timestamp: ' . $timestamp; $headers[] = 'X-Request-Signature: ' . $signature;
We'll add the various instance variables to the class:
79
protected $base_url = 'http://localhost/ codeigniter-handbook-vol-2/index.php/'; protected $access_token = '4395dd07a3cfe84d9655bb2542907f3acd0024fe'; protected $shared_secret = '3c697e1314808f56bd44bc5ccb4765607b433715';
Now we've got all our headers ready, let's set them:
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
Perform the request:
$result = curl_exec($curl);
Grab the request info (HTTP codes and such):
$info = curl_getinfo($curl);
…and return the result (remembering to decode the JSON):
return array( 'info' => $info, 'body' => json_decode($result) );
80
Excellent. Now we have a working request() method.
If we now run our tests using the phpunit command line, our test should pass:
$ phpunit --colors tests/trackers_test.php PHPUnit 3.6.12 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 3 assertions)
We can repeat this across our application and end up with a great, comprehensive suite of code that tests our application properly. This then enables us to refactor existing code and make changes without having to manually test every single API endpoint. The world of PHP testing and PHPUnit is much more comprehensive, of course, but we'll get to that in Volume Three. Until then, I hope I've managed to show a quick example of how to use PHPUnit to test the basics of your API.
81
Summary In this book we've extended CodeIgniter and built a comprehensive framework and toolset for creating fast, robust, well tested RESTful APIs. We've examined the theoretical concepts behind REST and HTTP and how they affect us in our day-to-day development and consumption of APIs. We've built a powerful routing system for routing our requests based on HTTP method, and taken a look at responding with arbitrary content types.
We've versioned our API to ensure it remains backward compatible (and futureproof!) We've integrated exceptions and used them as a brilliantly extensible technique for when things go wrong. We've also taken a brief look at namespaces and how they can help us separate our code. We've secured our API using request signing and API access tokens, and we've ensured our API remains stable using method throttling. Finally, we've created a comprehensive set of debugging tools to enable us to fix a problem quickly when things go wrong.
I hope you've enjoyed reading Volume Two of The CodeIgniter Handbook. The trilogy concludes with Volume Three: a book all about unit testing with CodeIgniter, where we examine integrating PHPUnit with CodeIgniter and all the tricky bits that go along with it. We'll take a look at the differences between unit and functional testing, we'll look at Test-driven Development and how it can help us write cleaner and better business logic and we'll delve into the depths of PHPUnit and see how to best configure it.
If you spot any errors in this book, we'd sincerely appreciate it if you could submit it to the errata page at https://efendibooks.com/books/codeigniterhandbook/vol-2/errata/. Like before, if you have any questions or suggestions how to improve this book, please don't hesitate to contact me at
[email protected] or on Twitter, at @jamierumbelow.
Thanks for reading!
82
ef efendi endi book bookss Smart, succinct books for web developers
https://efendibooks.com