CakePHP: Build REST APIs with RestApi plugin

Narendra Vaghela
9 min readOct 23, 2016

--

A guide to build REST APIs in CakePHP 3 application using RestApi plugin.

In this part, we will setup a fresh CakePHP application and install RestApi plugin.

Setup the application

In this step, we will install the CakePHP using composer. For this tutorial, I will use “C:\wamp64\www\cakephp-rest-api” directory.

We will use a virtual host for our CakePHP application. Let’s name it restapi.cake.dev and point it to C:\wamp64\www\cakephp-rest-api\webroot.

For detailed guide on installation and configuration, please see the official documentation here.

If everything goes well, you can see a default welcome page of CakePHP at http://restapi.cake.dev in your browser.

Setup the database

For our application, we will use MySQL database. For now, just create a blank database and name it restapi.

Now configure the database credentials in your APP/config/app.php. All set.

Refresh http://restapi.cake.dev in your browser and you will see a message that CakePHP is able to connect to the database.

You can skip this step if you do not need a database 🙂

Install the RestApi plugin

Our basic application is ready now. Let’s install our RestApi plugin using composer. Run the following command,

composer require multidots/cakephp-rest-api

Once the installation is complete, load the plugin into your application by adding following line in your APP/config/bootstrap.php,

Plugin::load(‘RestApi’, [‘bootstrap’ => true]);

You can also load the plugin using in built shell command like,

$ bin/cake plugin load -b RestApi

That is it. We are now ready to go.

Plugin configuration

By default, the RestApi plugin is configured to allow CORS requests. You can override these settings by creating api.php file in APP/config. It should look like,

<?php

return [
'ApiRequest' => [
'jwtAuth' => [
'enabled' => true,
'cypherKey' => 'R1a#2%dY2fX@3g8r5&s4Kf6*sd(5dHs!5gD4s',
'tokenAlgorithm' => 'HS256'
],
'cors' => [
'enabled' => true,
'origin' => '*',
'allowedMethods' => ['GET', 'POST', 'OPTIONS'],
'allowedHeaders' => ['Content-Type, Authorization, Accept, Origin'],
'maxAge' => 2628000
]
]
];

Modify the options as per your need.

A sample API method

Now everything is set and configured. Let’s create a first API method. Our endpoint will be http://restapi.cake.dev/foo/bar.

By default, CakePHP routing will look for Foo controller and bar() method for above url. So, let’s create FooCotroller first.

<?php

namespace App\Controller;

use RestApi\Controller\ApiController;

/**
* Foo Controller
*
*/
class FooController extends ApiController
{

/**
* bar method
*
* @return Response|null
*/
public function bar()
{
// your action code will go here
}
}

Please note that our FooController extends to ApiController and not the default AppController. This ApiController comes from RestApi plugin and we have added that on top of the class file. The ApiController provides all the necessary things required to make our API method working.

Now, run http://restapi.cake.dev/foo/bar and you will see following response.

{“status”:”NOK”,”result”:{“error”:”Token is missing. Please pass the token in request in the form of header, query parameter or post data field.”}}

This is because our plugin is set to check auth token in every request. You can disable this token check functionality either completely disabling it from configuration file or define a flag in your specific route.

Disable token check from configuration file

Update your api.php file and set ApiRequest.jwtAuth.enabled to false. So, your file will look like,

<?php

return [
'ApiRequest' => [
'jwtAuth' => [
'enabled' => false,
'cypherKey' => 'R1a#2%dY2fX@3g8r5&s4Kf6*sd(5dHs!5gD4s',
'tokenAlgorithm' => 'HS256'
],
'cors' => [
'enabled' => true,
'origin' => '*',
'allowedMethods' => ['GET', 'POST', 'OPTIONS'],
'allowedHeaders' => ['Content-Type, Authorization, Accept, Origin'],
'maxAge' => 2628000
]
]
];

Disable token check from route

Add allowWithoutToken parameter in your route and set it to true.

$routes->connect(‘/foo/bar’, [‘controller’ => ‘Demo’, ‘action’ => ‘foo’, ‘allowWithoutToken’ => true]);

After disabling auth token check, run http://restapi.cake.dev/foo/bar again and you will see following response.

{“status”:”OK”,”result”:{“message”:”Empty response!”}}

Bingo! It works 🙂

Now, let’s prepare a valid response. Update our bar() action like,

public function bar()
{
// movie list
$movies = [
'Captain America: Civil War',
'The Wave',
'Deadpool'
];

$this->apiResponse['movies'] = $movies;
}

And now it will return a list of movies in response.

{“status”:”OK”,”result”:{“movies”:[“Captain America: Civil War”,”The Wave”,”Deadpool”]}}

The important thing to note here is $this->apiResponse. Anything you assign to this variable will be returned in response.

Easy enough, right?

You can put your own logic inside the action, set the response and the plugin will take care of rest.

Okay, so let’s start with user registration and login APIs. We will use AccountController to handle these APIs. So, create a new controller at src/Controller/AccountController.php like,

<?php

namespace App\Controller;

use Exception;
use RestApi\Controller\ApiController;
use RestApi\Utility\JwtToken;

/**
* Account Controller
*
*/
class AccountController extends ApiController
{

/**
* Login method
*
* @return void
*/
public function login()
{
$this->request->allowMethod('post');
// login code will go here
}

/**
* Register method
*
* @return void
*/
public function register()
{
$this->request->allowMethod('post');
// registration code will go here
}
}

We need a database table to store user’s data. Let’s create a users table with bas

CREATE TABLE IF NOT EXISTS `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(50) NOT NULL,
`status` tinyint(1) NOT NULL DEFAULT '1',
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Based on above table, create Table and Entity classes. See below for example.

src\Model\Table\UsersTable.php

<?php

namespace App\Model\Table;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

/**
* Users Model
*
* @mixin \Cake\ORM\Behavior\TimestampBehavior
*/
class UsersTable extends Table
{

/**
* Initialize method
*
* @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config)
{
parent::initialize($config);

$this->table('users');
$this->displayField('name');
$this->primaryKey('id');

$this->addBehavior('Timestamp');
}

/**
* Default validation rules.
*
* @param Validator $validator Validator instance.
* @return Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->allowEmpty('id', 'create');

$validator
->requirePresence('name', 'create', 'This is required parameter.')
->notEmpty('name', 'Name is required.');

$validator
->email('email', 'Please provide a valid email address')
->requirePresence('email', 'create', 'This is required parameter.')
->notEmpty('email', 'Email address is required.');

$validator
->requirePresence('password', 'create', 'This is required parameter.')
->notEmpty('password', 'Password is required.');

$validator
->boolean('status')
->allowEmpty('status', 'create');

return $validator;
}

/**
* Default validation rules.
*
* @param Validator $validator Validator instance.
* @return Validator
*/
public function validationLoginApi(Validator $validator)
{
$validator
->email('email', 'Please provide a valid email address.')
->requirePresence('email', 'create', 'This is required parameter.')
->notEmpty('email', 'Email address is required');

$validator
->requirePresence('password', 'create', 'This is required parameter.')
->notEmpty('password', 'Password is required.');

return $validator;
}

/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* @param RulesChecker $rules The rules object to be modified.
* @return RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['email'], 'The email address is already in use. Please use a different email address.'));

return $rules;
}

/**
* Modifies password before saving into database
*
* @param Event $event Event
* @param EntityInterface $entity Entity
* @param ArrayObject $options Array of options
* @return bool
*/
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
{
if (isset($entity->password)) {
$entity->password = md5($entity->password);
}

return true;
}
}

And src\Model\Entity\User.php,

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

/**
* User Entity
*
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property bool $status
* @property \Cake\I18n\Time $created
* @property \Cake\I18n\Time $modified
*/
class User extends Entity
{

/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array
*/
protected $_accessible = [
'*' => true,
'id' => false
];

/**
* Fields that are excluded from JSON versions of the entity.
*
* @var array
*/
protected $_hidden = [
'password'
];
}

Now, the basic things are ready to implement registration and login APIs.

User Registration API

First, let’s create a route for this API. The endpoint will be http://restapi.cake.dev/account/register. Set following route in your APP/config/routes.php file,

$routes->connect('/account/register', ['controller' => 'Account', 'action' => 'register', 'allowWithoutToken' => true]);

Now, let’s update our register() method code. It will now look like,

/**
* Register method
*
* Returns a token on successful registration
*
* @return void
*/
public function register()
{
$this->request->allowMethod('post');

$this->loadModel('Users');

$user = $this->Users->newEntity($this->request->data());
try {
if ($this->Users->save($user)) {
$this->apiResponse['message'] = 'Registered successfully.';
$payload = ['email' => $user->email, 'name' => $user->name];
$this->apiResponse['token'] = JwtToken::generateToken($payload);
} else {
$this->httpStatusCode = 400;
$this->apiResponse['message'] = 'Unable to register user.';
if ($user->errors()) {
$this->apiResponse['message'] = 'Validation failed.';
foreach ($user->errors() as $field => $validationMessage) {
$this->apiResponse['error'][$field] = $validationMessage[key($validationMessage)];
}
}
}
} catch (Exception $e) {
$this->httpStatusCode = 400;
$this->apiResponse['message'] = 'Unable to register user.';
}

unset($user);
unset($payload);
}

So, our register() method only allows POST request. Also note that, to generate a JWT token we have used an JwtToken utility class from RestApi plugin. We have passed email and name as a payload data to generate token. You can add whatever fields you want to generate token.

The response should look like,

{
"status": "OK",
"result": {
"message": "Registered successfully.",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5hcmVuZHJhQGV4YW1wbGUuY29tIiwibmFtZSI6Ik5hcmVuZHJhIn0.Cz8MxCUz6GbsQ698IVIm_v7v40jaCj9s_F0FvS6Ya2k"
}
}

Bingo!

You can store this token and use it in further API requests which requires token. We will see it later.

Now, try with few more requests to check how this method works. For example, if you submit a new request with same email address, it will response with validation errors. See below example response,

{
"status": "NOK",
"result": {
"message": "Validation failed.",
"error": {
"email": "The email address is already in use. Please use a different email address."
}
}
}

User Login API

Similar to registration api, we will use http://restapi.cake.dev/account/login as API endpoint. Set following route in your APP/config/routes.php file,

$routes->connect('/account/login', ['controller' => 'Account', 'action' => 'login', 'allowWithoutToken' => true]);

Now, modify the login() method like,

/**
* Login method
*
* @return void
*/
public function login()
{
$this->request->allowMethod('post');
$this->loadModel('Users');$entity = $this->Users->newEntity($this->request->data, ['validate' => 'LoginApi']);if ($entity->errors()) {
$this->httpStatusCode = 400;
$this->apiResponse['message'] = 'Validation failed.';
foreach ($entity->errors() as $field => $validationMessage) {
$this->apiResponse['error'][$field] = $validationMessage[key($validationMessage)];
}
} else {
$user = $this->Users->find()
->where([
'email' => $entity->email,
'password' => md5($entity->password),
'status' => 1
])
->first();
if (empty($user)) {
$this->httpStatusCode = 403;
$this->apiResponse['error'] = 'Invalid email or password.';
return;
}
$payload = ['email' => $user->email, 'name' => $user->name];$this->apiResponse['token'] = JwtToken::generateToken($payload);
$this->apiResponse['message'] = 'Logged in successfully.';
unset($user);
unset($payload);
}
}

Similar to register api, this method allows only POST request. Now make a POST request to http://restapi.cake.dev/account/login with email and password that we have used in our registration API example.

And it will return below response with token.

{
"status": "OK",
"result": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5hcmVuZHJhQGV4YW1wbGUuY29tIiwibmFtZSI6Ik5hcmVuZHJhIn0.Cz8MxCUz6GbsQ698IVIm_v7v40jaCj9s_F0FvS6Ya2k",
"message": "Logged in successfully."
}
}

That is it. You can play around the example, update validations, modify code etc. etc.

User auth token in request

In our previous part, we made a sample API method which was returning a list of movies. For testing purpose, we have disabled the auth token check from that API request.

Let’s enable the token check. And to do that, remove allowWithoutToken parameter from route or set it to false.

$routes->connect('/foo/bar', ['controller' => 'Foo', 'action' => 'bar', 'allowWithoutToken' => false]);

Now, make a GET request to http://restapi.cake.dev/foo/bar and it will return error response like below.

{
"status": "NOK",
"result": {
"error": "Token is missing. Please pass the token in request in the form of header, query parameter or post data field."
}
}

So, here we need to pass the auth token in request parameter by either using header, GET parameter or POST data field.

Let’s pass it in GET parameter, so our endpoint URL will be,

http://restapi.cake.dev/foo/bar?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5hcmVuZHJhQGV4YW1wbGUuY29tIiwibmFtZSI6Ik5hcmVuZHJhIn0.Cz8MxCUz6GbsQ698IVIm_v7v40jaCj9s_F0FvS6Ya2k

And it will return the response with movie list like below.

{
"status": "OK",
"result": {
"movies": [
"Captain America: Civil War",
"The Wave",
"Deadpool"
]
}
}

You can also pass this token in header.

Access user data from token

It is important to know that which user is making an API request. In our case, we are using JWT token to identify the user who is making a request. The RestApi plugin decodes the token and set the payload data in jwtPayload variable. You can also access the token using jwtToken variable. Let’s modify our example API request and return the payload data in response.

<?phpnamespace App\Controller;use RestApi\Controller\ApiController;/**
* Foo Controller
*
*/
class FooController extends ApiController
{
/**
* bar method
*
* @return Response|null
*/
public function bar()
{
// movie list
$movies = [
'Captain America: Civil War',
'The Wave',
'Deadpool'
];
$this->apiResponse['movies'] = $movies;
// set payload
$this->apiResponse['payload'] = $this->jwtPayload;
// set token
$this->apiResponse['token'] = $this->jwtToken;
}
}

And when you make the request again, it will return the response like below.

{
"status": "OK",
"result": {
"movies": [
"Captain America: Civil War",
"The Wave",
"Deadpool"
],
"payload": {
"email": "narendra@example.com",
"name": "Narendra"
},
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5hcmVuZHJhQGV4YW1wbGUuY29tIiwibmFtZSI6Ik5hcmVuZHJhIn0.Cz8MxCUz6GbsQ698IVIm_v7v40jaCj9s_F0FvS6Ya2k"
}
}

You can use the payload data in your logic wherever required.

That is it for now. Enjoy!

--

--

Narendra Vaghela

Co-founder at SprintCube & Solution Architect | CakePHP Developer