RESTful API with CakePHP (2.9)

I recently had to add some API endpoints to an existing web site built three or four years ago in CakePHP 2. Here’s how I did it and how I overcame some challenges along the way.

The current site was pretty much a normal MVC app that generates web pages, so adding API endpoints was a complete departure. The new endpoints would be consumed by mobile apps and would return just data, rather than HTML. After working a few things out, it was very simple.

I should mention that there was not enough time to upgrade the app to version 3 of CakePHP, so this article does not apply to the latest version (but it’s probably not that far off).

TL;DR

Here’s a recap of the steps I took to enable API endpoints in my CakePHP application (all detailed later in this post):

  • Added the RequestHandler component to /Controller/AppController
  • Added mapResources and parseExtensions to /Config/routes.php
  • Set up new controllers with vanilla CRUD actions specifically to handle API endpoints
  • Created json subfolders with the view directories mapped to the API enabled controllers
  • Created a _serialize variable within my controller actions
  • Created view files within the json subfolders that mapped to my controller actions
  • Dynamically changed the authentication method to Basic on API calls
  • Added an unauthorizedRedirect key to the Auth component
  • Created an access_denied action the UsersController as well as the required views
  • Dynamically made sure that an unauthorised json request renders the json version of the access_denied view
  • Made sure that all API requests use the HTTPS protocol
  • Deployed the Security component
  • Tested my endpoints with Postman

The basics

The URL your apps will consume will (or can) be exactly the same as your current URLs, but will have a suffix added to them. This is what distinguishes them as API calls. Most likely that will be .json (which I’ll work with for this article) or perhaps .xml.

For example, mysite.com/users will become mysite.com/users/index.json. Both would access the exact some controller action. Equally, mysite.com/users/view/123 would become mysite.com/users/view/123.json.

With that in mind, you need to allow your controllers to handle those json requests and send the correct response. You do that by adding the RequestHandler component. Like any component, you could add it to discrete controllers or to all by including it AppController. Wherever you do it, here is the code:

public $components = [
'...YourCurrentComponents',
'RequestHandler'
];

The request handler also sets the correct HTTP status code as part of the response (e.g. 200 when everything is fine, 500 when there is a server error or you write bad code, 404 when the page is not found etc.).

Routing

Now you need to adapt your routes to process the json requests. You need to add the following lines before the last line in /Config/routes.php:

// These must go above the existing last line
Router::mapResources(['users']);
Router::parseExtensions();
// The current last line stays in place
require CAKE . ‘Config’ . DS . ‘routes.php’;

These two lines do two things:

  1. mapResources sets up some default routes for REST access (explained below); and
  2. parseExtensions looks at the suffix in the URL and routes the response correctly.

Note that I have passed a controller name into the mapResources function. You don’t have to do that, but it does give some sense of control. In my example, only REST requests to the users controller are going to be mapped.

RESTful verbs

You might be aware that REST uses a standard set of ‘verbs’ to determine how to handle requests. The common ones are:

  • GET (used for simply retrieving data; e.g. index)
  • POST (used for adding new records; e.g. add)
  • PUT (used for updating records; e.g. edit)
  • DELETE (used for deleting records; e.g. delete)

The key difference is that (much like your standard controller actions) GET and DELETE are URL based, whilst POST and PUT also expect key/value pairs in the request body. Think of this as your posted data.

This is important because Cake will automatically map the request type to known controller actions:

  • GET maps to the /index action unless there is also a parameter, in which case it maps to the /view action. So GET /users maps to /users/index and GET /users/123 maps to /users/view/123
  • POST maps to the /add action
  • PUT maps to the /edit action
  • DELETE maps to the /delete action

In other words, it is the REST verb that determines the controller action called, not the route. So expecting a POST request to /users/change_password to map to the/users/change_password controller action will fail. It’s a POST request so it will map to users/add.

You can override this behaviour by modifying the default routes, but I found this unecessary. In my use cases I found that the REST calls were doing quite distinct things, so routing them to the existing controller actions didn’t make sense.

For example, I have a Transactions controller that can handle different types of transaction such as transfer_funds, withdraw_funds and make_a_payment. There isn’t an ‘add’ action because it doesn’t make sense, and adding one just for the API didn’t make sense either, not least because ‘add’ doesn’t describe what is actually happening.

One endpoint I wanted was — effectively—‘transactions/sell_credit’, which is a POST request. Rather than shoehorn that in to the Transactions controller and set up special routing, I chose to create a few new controllers specifially for REST. So what would have been transactions/sell_credit became credits/add. Now any POST request to /credits/add is handled by the add action of my new CreditsController.

As the business logic sat in my Transaction model I simply added public $uses = ['Transaction']; to the top of my controller and everything just worked.

Requests

By now, your API calls will be hitting your controller actions. You can get to command line parameters and posted data exactly as if they were originating from a standard browser request. Process it as you normally would and we can move on to responses and views.

Responses and views

When you add a suffix, you are also determining the format of the response. So a .json request should return json data.

Your controller action will process the request and — just as when rendering an HTML view — will set some variables that contain data to be processed in thew view template and rendered on the web page.

If you just set up the routes and handle the request appropriately your controller will still look for a view file that matches the url. So /users/index will look for /View/Users/index.ctp. If you do nothing else, your REST request will return the HTML defined by that view, which will include your layout and everything else — just as if it were a web page. That’s no good for the app consuming the response because all it wants if the json data. So you need to ensure that you return just json data by serializing the variables passed to the view and by creating JSON and XML views.

Serializing your variables is simple. Let’s say you have a variable called $user that is normally passed to the view template. To create a json version you add an extra line to the controller action:

$this->set([
'user' => $user,
'_serialize' => ['user']
]);

The $user variable will now contain a json version of $user.

Rendering the data via a view

The serialized variable is now available to your view template, but is of no use to a normal HTML template file. You need to create json specific view template files for the json controller actions.

For each controller action that is accessed via REST, create a subfolder that matches the suffix. So for /users/index.json you create the following file:

/View/Users/json/index.ctp

The content of the view file is very simple:

<?php
echo json_encode(compact('user'));

And that’s it. Your app will now receive a well formed json response that it can consume as it pleases. That is sent as a response to the consuming app and you have returned json data.

Authentication and authorisation

Just for clarity (because I think it’s sometimes misunderstood) ‘authentication’ is the process of identifying a user (logging in) and ‘authorisation’ is determining whether the current user can do what they are trying to do (permissions).

Authentication

If your API endpoints do not require authentication (i.e. they are freely and publicly accessible) you simply allow them as you would any other URL (probably using $this->Auth->allow(); or excluding Auth altogether). If they do need authentication you need to adapt your app a bit.

Your CakePHP app probably asks users to log in by completing a form containing username and password fields. This is called ‘Form Based Authentication’. That’s not practical when the resource is being accessed via an API call, so you need something different.

In the world of REST and mobile apps, authentication is slightly different to normal web based apps. Generally speaking your CakePHP application should be ‘stateless’. That is to say that the CakePHP app retains no persistent knowledge of who the user accessing the REST calls is (although it can), which means that the app needs the ability to log in on each call.

The mobile app doesn’t want to present a log in screen for each request so it has to pass the username and password along with each request. There are different ways of authenticating users; I chose Basic Authentication as it works for both the CakePHP app and the mobile app consuming the API.

I would stress here that when using Basic authentication you must use HTTPS, else the user’s credentials are passed across in plain text.

Setting up Basic authentication is fairly simple but I learned a trick here. You can do this in AppController.php:

$this->Auth->authenticate = [
'Form' => [
'passwordHasher' => 'Blowfish'
],
'Basic' => [
'passwordHasher' => 'Blowfish'
]
];

Side note: I am using Blowfish as the password encryption method (which I recommend) but you don’t have to.

What this says is “try Form authentication first and stop if the user successfully logs in, otherwise try Basic authentication”. The gotcha here is that if a user enters the wrong password in your website’s log in form, rather than presenting a message and asking them to try again they see an HTTP type log in screen; that’s Basic authentication kicking in. Not good.

What I really wanted to achieve is “always use Form authentication (and just Form authentication) unless it’s an API call (identified by the json/xml suffix) in which case switch to Basic authentication”. Here’s how I did that:

// In AppController->beforeFilter()
if ($this->request->is('json') || $this->request->is('xml')) {
$this->Auth->authenticate = [
'Basic' => [
'passwordHasher' => 'Blowfish'
]
];
}

What this does is change the authenticate method to Basic when the request is an API call, otherwise it sticks with Form authentication.

Authorisation

When you need authorisation, do what you normally do with the Auth component and — perhaps — ACLs. The endpoints accessed via REST will conform to your current allow/deny rules. If — like me — you set up new controllers and actions, don’t forget to add the permissions for those.

Your normal controller actions probably redirect on authorisation failure and present flash messages on the next page. Again, this doesn’t work with API access. If authorisation fails you really want to return a response so that the app can handle it appropriately. Here’s what I did and how I overcame another gotcha.

First, create a controller action and suitable view for when authorisation fails. Here’s what I did:

// In UsersController.php
public function access_denied()
{
$loggedIn = $this->Auth->user('id');
    $response = [
'result' => false,
'code' => 'access-denied',
'message' => 'Invalid credentials or access denied.'
];
    $this->set(compact('loggedIn', 'response'));
$this->set('_serialize', ['loggedIn', 'response']);
}

What this does is set a variable to determine if the user is logged in or not and then an array of values that help me present information to the user describing what has happened. Note that I am serializing them too.

Secondly, add an ‘unauthorizedRedirect’ key to the Auth component:

// In AppController.php
public $components = [
...//all your other components,
'Auth' => [
...//all your existing Auth settings,
'unauthorizedRedirect' => [
'admin' => false,
'controller' => 'users',
'action' => 'access_denied'
]
]
];

What this does is redirects the user to /users/access_denied when they try to access a controller action they do not have permission for. You can shape the view in a way that suits you.

Third, create json/xml versions of the view as well. Here’s my json view:

// /View/Users/json/access_denied.ctp
<?php
echo json_encode(compact('response'));

This works, well, but there’s a gotcha. Note that the route to the access_denied controller function does not have an extension. This means that in every case the normal HTML view is rendered rather than the json version. Here’s how I got around that:

// In AppController->beforeFilter()
if (! empty($this->request->params['ext'])) {
$this->Auth->unauthorizedRedirect['ext'] = $this->request->params['ext'];
}

What it does is check the ‘ext’ key of Request->params. If it’s set (in other words if it’s from an API call) alter the route of the access_denied function to include the extension (json) and render that view instead. Now, when a user makes an unauthorised request to the API it returns a json object rather than HTML. As with everything, this might be a dreadful hack with a much more beautiful solution, but it works.

Security

It is imperative that all API requests are called via SSL over HTTPS, especially as the user credentials would otherwise be transmitted as plain text.

I would also recommend using the Security component although bear in mind that simply adding it can have hard to identify and crack knock on effects elsewhere, especially if you use javascript to change forms on the fly.

Testing

You can’t easily test API calls from the browser’s address bar (other than simple GET requests when the user does not need to log in). I used a Chrome plugin called Postman. This allows you type the full URL into an address bar as well as set authorization credentials and set up key/value pairs in the request body (your posted data). I won’t go into a full tutorial here.

What I did find is that API testing using localhost with MAMP over HTTPS was not straightforward. In fact it didn’t work. To get around this I had to create a specific host (something.local) and enable SSL on that with a self-signed certificate. First access the site in your browser to be sure it works, and trust the certificate when requested. Then Postman testing shoud work too.

I ried and failed to set up normal unit tests using HttpSocket, but they crashed and burned miserably. Maybe I wasn’t doing it correctly. However, all of my business logic is in the model layer and I have extensive tests on those methods. So I’m comfortable that a combination of Postman tests and model layer unit tests has me covered. You can set up test suites in Postman too, but I didn’t get that far.

Conclusion

I stumbled across a few blockers while building my API endpoints and had to resolve some of them myself as I couldn’t easily find assistance on line, which prompted me to write it up. I hope it helps you. Please feel free to ask questions in the comments.