A complete RESTful service and client in CakePHP tutorial

in CakePHP/Tutorials & Samples

Last weekend, I spent 2 frustrating days trying to get a RESTful application up and running with CakePHP! Once again, the good folks at CakePHP have documentation, but they miss some important things that caused me massive headaches. After this long and painful process was finished, I promised myself to write a tutorial on how to create a RESTfull application in CakePHP and also how to write a simple client to interact with the RESTfull application. You can download the full tutorial here and you are free to modify it however you like. The tutorial is based on CakePHP 2.4 (Please note that I am not changing the default CakePHP theme. So below is what the index page looks like:)

rest-service-screenshot

Step1: The Database Structure

For our REST application, we will be creating a Phones listing application. The application will allow users to list/edit/view/delete and add phones. Here is the database structure:

Let’s start by creating the phones database table. The phones table contains a phone’s name, manufacturer and description. Here is what the final phones table looks like:

CREATE TABLE phones (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50),
	manufacturer VARCHAR(50),
    description TEXT,
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);			
			

Next I populate the database with 4 entries. (The top 4 phones at moment)

INSERT INTO phones (name,manufacturer,description,created)
    VALUES ('iPhone 5s', 'Apple', 'The iPhone 5s may look a lot like its predecessor. But with a faster new processor, a fingerprint sensor, and an improved camera flash, it is a serious upgrade.', NOW());
INSERT INTO phones (name,manufacturer,description,created)
    VALUES ('Nexus 5','Google', 'Running the latest version of Android, the Nexus 5 really shows off the best aspects of Googles mobile OS. There are few reasons not to pick the Nexus 5 as your next Android phone.', NOW());
INSERT INTO phones (name,manufacturer,description,created)
    VALUES ('One','HTC', 'With its stellar design, great camera, and hardy processor, the HTC One is the phone to beat.', NOW());
INSERT INTO phones (name,manufacturer,description,created)
    VALUES ('Galaxy S4','Samsung', 'The Samsung Galaxy S4 is a stellar Android phone held back by boring design and half-baked features.', NOW());
			

Step 2: Create The Phones Model, Controller and View

The next thing we need to do is create the phones model controller and view. This is pretty standard and basic stuff in Cake. Although I have omitted the view files, you can view the phones model and controller below. The views files can be found in the tutorial download itself.

Phones Model

class Phone extends AppModel {
}
		

Phones Controller

class PhonesController extends AppController {
    public $helpers = array('Html', 'Form');
	public $components = array('RequestHandler');

    public function index() {
         $this->set('phones', $this->Phone->find('all'));
    }
	
	public function add() {
        if ($this->request->is('post')) {
            $this->Phone->create();
            if ($this->Phone->save($this->request->data)) {
                $this->Session->setFlash(__('The phone has been saved.'));
                return $this->redirect(array('action' => 'index'));
            }
            $this->Session->setFlash(__('Unable to add your phone.'));
        }
    }

    public function view($id = null) {
        if (!$id) {
            throw new NotFoundException(__('Invalid phone'));
        }

        $phone = $this->Phone->findById($id);
        if (!$phone) {
            throw new NotFoundException(__('Invalid phone'));
        }
        $this->set('phone', $phone);
    }
	
	public function edit($id = null) {
		if (!$id) {
			throw new NotFoundException(__('Invalid phone'));
		}

		$phone = $this->Phone->findById($id);
		if (!$phone) {
			throw new NotFoundException(__('Invalid phone'));
		}

		if ($this->request->is(array('phone', 'put'))) {
			$this->Phone->id = $id;
			if ($this->Phone->save($this->request->data)) {
				$this->Session->setFlash(__('Your phone has been updated.'));
				return $this->redirect(array('action' => 'index'));
			}
			$this->Session->setFlash(__('Unable to update your phone.'));
		}

		if (!$this->request->data) {
			$this->request->data = $phone;
		}
	}
	
	public function delete($id) {
		if ($this->request->is('get')) {
			throw new MethodNotAllowedException();
		}

		if ($this->Phone->delete($id)) {
			$this->Session->setFlash(
				__('The phone with id: %s has been deleted.', h($id))
			);
			return $this->redirect(array('action' => 'index'));
		}
	}
}		
		

Step 3: Setup Restfullness

Now, its time to make our phones application “RESTful”. The first thing we need to do is modify our routes.php file by adding 2 lines.

//In app/Config/routes.php...
Router::mapResources('phones');
Router::parseExtensions();
			

The first line calls the function mapResources() which specifies the controllers that should support REST. Two things need to be noted here:

  1. It requires the url-ized version of your controller as a parameter. So “Phones” becomes “phones” and if you had a controller named “LiveCustomers”, it would become “live_customers”.
  2. I didn’t put the Phones controller but instead a controller called RestPhones. This will be explained later..

The second line tells Cake to start parsing extensions. So CakePHP will now be able to respond to actions that have extensions. By default, CakePHP will support XML, JSON and RSS as extensions. So, clients will be able to call for a JSON output by calling action.json and an XML output by calling action.xml. It’s actually pretty cool.

One important thing to remember is that the piece of code above to support REST routing must be defined before this line:

		require CAKE . 'Config' . DS . 'routes.php';
		

Not doing so will cause you several hours of painful debugging. Trust me, it happened to me…

Step 4: Understanding CakePHP REST routes

If you know anything about REST, then you are aware that it only supports standard HTTP requests such as POST, GET, PUT and DELETE. What CakePHP does is match various controller actions to REST routes and also to specific HTTP requests. (This is actually pretty ingenious!) So, if you had a controller that is configured to accept REST calls, then you have to follow the convention and specify the proper HTTP format for the proper controller action. For example, to list all the phones in our database, we need to create a controller action called index() and its url will have to be /phones.format and it will also only accept GET requests. Here is the standard table ripped out of Cake 2.0 Book:

HTTP format

URL.format

Controller action invoked

GET

/rest_phones.format

RestPhonesController::index()

GET

/ rest_phones/123.format

RestPhonesController::view(123)

POST

/ rest_phones.format

RestPhonesController::add()

PUT

/ rest_phones/123.format

RestPhonesController::edit(123)

DELETE

/ rest_phones/123.format

RestPhonesController::delete(123)

POST

/ rest_phones/123.format

RestPhonesController::edit(123)

As The Cake book mentions: These routes are HTTP Request Method sensitive.. In other words, these routes are sensitive to the HTTP request method that you specify. So, trying to request a post to the index() controller will generate an error. If you are not happy with the default REST routes, cake allows you to change them as well. (I personally think that the default CakePHP routes are perfect and should not be changed…) Once again, I am using a controller called RestPhonesController that I still have not explained. Well lets explain why I am using this mystery controller…

Step 4: Understanding CakePHP 2.0 REST limitations

In order to explain why I have the RestPhonesController class, we need to talk about limitations that CakePHP has:

1) MapResources() does not support prefixes!!!: A clean API for REST should have clean URLs! So, in the case of my phones controller’s index function, it would be nice if I could create a function called index() for regular web viewing and then rest_index() for REST calls to the application. Then using, prefix routing, I could tell CakePHP that any controller in PhonesController that are prefixed by rest_ should get converted to /rest/ in their URL. So rest_view() would map to the url /rest/view/. However, CakePHP 2.0’s mapeResources function does not allow this! In fact, it took me a full day to find this out!!! But once I discovered this, I found other fellow developers who were bitching at the same issue… (Thanks for the discovery, Rudy Lee!) Apparently, this issue will be fixed for CakePHP 3.0 but I cant wait that long. There is also a hack out there involving modifying the core of CakePHP but I am against hacking the core of any framework. You should never hack your framework to get things to work…

2) You must explicitly specify which HTTP actions are allowed to pass!!!: As all good web developers know, you should only use https and secured connections to interact with REST services. This protects your data and protects you from man-in-the-middle attacks. In order to do this with CakePHP, you need to enable CakePHP’s security component. However, This was another half-day wasted in discovering that CakePHP’s security component does not play nice with REST clients. SecurityComponent needs to know which HTTP requests that it is allowed to accept from clients! By default, Cake’s Security component will “BLACKHOLE” all HTTP requests unless you explicitly specify which ones should pass through. (I discovered this one thanks to MetZ.)

So how do we get around these problems…?

Step 5: Create A RESTful Controller

The reason why RestPhonesController exists is that it allows me to provide a clean REST service that is separate from the regular application. It also allows me to add the prefix convention that I want so that I can separate the regular browser calls from the REST API calls. RestPhonesController basically uses the same model as PhonesController (Phones model) but it can be customized for whatever limitations or additions that we want to give our RESTful service. For example, imagine that on the web application, you can edit any phone but on the RESTful service, you only want the user to be able to edit a limited number of phones… By having RestPhonesController, I also follows all of CakePHP’s REST routes. So now, I have actions that are REST-specific and also follow proper routing. Here is the full code for RestPhonesController:

class RestPhonesController extends AppController {
	public $uses = array('Phone');
    public $helpers = array('Html', 'Form');
	public $components = array('RequestHandler');


	public function index() {
		$phones = $this->Phone->find('all');
        $this->set(array(
            'phones' => $phones,
            '_serialize' => array('phones')
        ));
    }

	public function add() {
		$this->Phone->create();
		if ($this->Phone->save($this->request->data)) {
			 $message = 'Created';
		} else {
			$message = 'Error';
		}
		$this->set(array(
			'message' => $message,
			'_serialize' => array('message')
		));
    }
	
	public function view($id) {
        $phone = $this->Phone->findById($id);
        $this->set(array(
            'phone' => $phone,
            '_serialize' => array('phone')
        ));
    }

	
	public function edit($id) {
        $this->Phone->id = $id;
        if ($this->Phone->save($this->request->data)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }
	
	public function delete($id) {
        if ($this->Phone->delete($id)) {
            $message = 'Deleted';
        } else {
            $message = 'Error';
        }
        $this->set(array(
            'message' => $message,
            '_serialize' => array('message')
        ));
    }
}		
		

One important thing that you will notice in RestPhonesController is that I added the following line to call the RequestHandlder component of CakePHP:

			public $components = array('RequestHandler');</p>
		

The requesthandler is the component that will handler all REST HTTP requests that are received and perform the appropriate action based on the provided url. So, for example, A POST to /rest_phones.json will create a new Phone and return JSON data to the client. A call GET to /rest_phones.xml will get all the Phones in the system and return XML data to the client. You can refer to the table above to see what URL path should be used for what HTTP action..

Step 5: Update AppController

Now, in order to split requests coming in, I have modified AppController’s beforeFilter() method to determine if we are rest controller or not. If we are the RestPhonesController, we tell the security component to unlock the various REST actions for the HTTP requests, otherwise. Here is my code for beforeFilter:

	public function beforeFilter() {

		if(in_array($this->params['controller'],array('rest_phones'))){
			// For RESTful web service requests, we check the name of our contoller
			$this->Auth->allow();
			// this line should always be there to ensure that all rest calls are secure
			/* $this->Security->requireSecure(); */
			$this->Security->unlockedActions = array('edit','delete','add','view');
			
		}else{
			// setup out Auth
			$this->Auth->allow();			
		}
    }
		

Note that although I am using the Auth component, it allows all actions! You will most likely need to modify this to suit your needs. You will have to modify the provided code to support authentication functions such as login, logout, etc…

Step 6: Setup a Test Client

I took the lazy approach with my test client and simply integrated it directly into the same application. I did this by creating a controller called ClientController. This controller uses CakePHP’s HttpSocket class to make HTTP request to the REST service. The client allows you to add/edit/view/delete phones by making the proper REST calls. For every call, the client will display the response code as well as the response content for any HTTP request that is made. This client can be run from a separate application on a different server to truly test communication between the client and the REST service. The following is the code for the ClientController:

App::uses('HttpSocket', 'Network/Http');
class ClientController extends AppController {
	public $components = array('Security', 'RequestHandler');
	
	public function index(){
		
	}

	public function request_index(){
	
		// remotely post the information to the server
		$link =  "http://" . $_SERVER['HTTP_HOST'] . $this->webroot.'rest_phones.json';
		
		$data = null;
		$httpSocket = new HttpSocket();
		$response = $httpSocket->get($link, $data );
		$this->set('response_code', $response->code);
		$this->set('response_body', $response->body);
		
		$this -> render('/Client/request_response');
	}
	
	public function request_view($id){
	
		// remotely post the information to the server
		$link =  "http://" . $_SERVER['HTTP_HOST'] . $this->webroot.'rest_phones/'.$id.'.json';

		$data = null;
		$httpSocket = new HttpSocket();
		$response = $httpSocket->get($link, $data );
		$this->set('response_code', $response->code);
		$this->set('response_body', $response->body);
		
		$this -> render('/Client/request_response');
	}
	
	public function request_edit($id){
	
		// remotely post the information to the server
		$link =  "http://" . $_SERVER['HTTP_HOST'] . $this->webroot.'rest_phones/'.$id.'.json';

		$data = null;
		$httpSocket = new HttpSocket();
		$data['Phone']['name'] = 'Updated Phone Name';
		$data['Phone']['manufacturer'] = 'Updated Phone  Manufacturer';
		$data['Phone']['name'] = 'Updated Phone  Description';
		$response = $httpSocket->put($link, $data );
		$this->set('response_code', $response->code);
		$this->set('response_body', $response->body);
		
		$this -> render('/Client/request_response');
	}
	
	public function request_add(){
	
		// remotely post the information to the server
		$link =  "http://" . $_SERVER['HTTP_HOST'] . $this->webroot.'rest_phones.json';

		$data = null;
		$httpSocket = new HttpSocket();
		$data['Phone']['name'] = 'New Phone';
		$data['Phone']['manufacturer'] = 'New Phone Manufacturer';
		$data['Phone']['name'] = 'New Phone Description';
		$response = $httpSocket->post($link, $data );
		$this->set('response_code', $response->code);
		$this->set('response_body', $response->body);
		
		$this -> render('/Client/request_response');
	}
	
	public function request_delete($id){
	
		// remotely post the information to the server
		$link =  "http://" . $_SERVER['HTTP_HOST'] . $this->webroot.'rest_phones/'.$id.'.json';

		$data = null;
		$httpSocket = new HttpSocket();
		$response = $httpSocket->delete($link, $data );
		$this->set('response_code', $response->code);
		$this->set('response_body', $response->body);
		
		$this -> render('/Client/request_response');
	}
}
		

And here is a screenshot of the index action for the CLientController.

rest-service-client-screenshot
rest-service-index-screenshot

Step 7: Remember to use a secure connection

A well-written RESTful application should only accept secure https connections!!! Although I have commented out the call to:

		$this->Security->requireSecure();
		

In line 46 of AppController’s beforeFilter() function, you should never go to production with this pieces of code turned off. This line of code tells the RESTfull application to blackhole all requests that are not over a secure connection! This is the safest way to protect your application from man-in-the-middle attacks!

Step 8: Download it all and start testing

That concludes the tutorial. You can downloadthe entire tutorial in zip format here. It was written using CakePHP 2.4 and I also include the SQL file used to create the phones database. Fell free to play with the client and modify the REST service as you like. If you’ve got questions or comments, hit me up. I’ll help if I can.

Tags:

Mifty Yusuf is a Montreal-based software developer who enjoys playing with new web technologies as well as comic books and illustrations. He beleives that, no matter what the question is, the answer is always Batman!

72 Comments

  1. Thanks for this article. How can i authenticate client request like token generate etc? Thanks in advance

Leave a Reply

Your email address will not be published.

*

Latest from CakePHP

Go to Top