Yii 1.1: How-To: Create a REST API

123 followers

This article will explain how to create a REST API with the Yii framework.

Information about REST

A good introduction about implementing REST service with PHP can be found on http://www.gen-x-design.com.

Usefull Tools

To fire up a REST request to a Yii application, you can use the Firefox REST Client Addon.

If you want to send REST requests via a console, perhaps you might check cUrl.

Requirements

We will create an API that allows us to

  • Get all items of a certain model
  • Get one single model item via its primary key (id)
  • Create a new item
  • Update an existing item
  • Delete an existing item.

In this tutorial, we will use the Yii Blog demo application. Speaking of a model here means the Post model, i.e. creating and reading post items

The API shall be flexible enough that it can be extended easily to work on more different models, e.g. comments or user data.

All requests to the API shall use an authorization.

All right, let's get things going!

Setting up the URL Manager

When using the API, we would like to have the following URL scheme:

  • View all posts: index.php/api/posts (HTTP method GET)
  • View a single posts: index.php/api/posts/123 (also GET )
  • Create a new post: index.php/api/posts (POST)
  • Update a post: index.php/api/posts/123 (PUT)
  • Delete a post: index.php/api/posts/123 (DELETE)

In order to parse these URL's, set up the URL manager in config/main.php like this:

...
'urlManager'=>array(
    'urlFormat'=>'path',
    'rules'=>array(
        'post/<id:\d+>/<title:.*?>'=>'post/view',
        'posts/<tag:.*?>'=>'post/index',
        // REST patterns
        array('api/list', 'pattern'=>'api/<model:\w+>', 'verb'=>'GET'),
        array('api/view', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'GET'),
        array('api/update', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'PUT'),
        array('api/delete', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'DELETE'),
        array('api/create', 'pattern'=>'api/<model:\w+>', 'verb'=>'POST'),
        // Other controllers
        '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
    ),
),
...

Note that for all requests, we will get the requested model (e.g. posts) via the GET model parameter.

For the Get Single Item and Update Item method, we will receive the model's primary key via the GET id parameter.

Create an API controller

In this tutorial, we will implement all REST methods in a new controller. Put this file in the controllers directory:

class ApiController extends Controller
{
    // Members
    /**
     * Key which has to be in HTTP USERNAME and PASSWORD headers 
     */
    Const APPLICATION_ID = 'ASCCPE';
 
    /**
     * Default response format
     * either 'json' or 'xml'
     */
    private $format = 'json';
    /**
     * @return array action filters
     */
    public function filters()
    {
            return array();
    }
 
    // Actions
    public function actionList()
    {
    }
    public function actionView()
    {
    }
    public function actionCreate()
    {
    }
    public function actionUpdate()
    {
    }
    public function actionDelete()
    {
    }
}

Implementing the Actions

Get all Models List Action

public function actionList()
{
    // Get the respective model instance
    switch($_GET['model'])
    {
        case 'posts':
            $models = Post::model()->findAll();
            break;
        default:
            // Model not implemented error
            $this->_sendResponse(501, sprintf(
                'Error: Mode <b>list</b> is not implemented for model <b>%s</b>',
                $_GET['model']) );
            Yii::app()->end();
    }
    // Did we get some results?
    if(empty($models)) {
        // No
        $this->_sendResponse(200, 
                sprintf('No items where found for model <b>%s</b>', $_GET['model']) );
    } else {
        // Prepare response
        $rows = array();
        foreach($models as $model)
            $rows[] = $model->attributes;
        // Send the response
        $this->_sendResponse(200, CJSON::encode($rows));
    }
}

Get a Single Model Action

public function actionView()
{
    // Check if id was submitted via GET
    if(!isset($_GET['id']))
        $this->_sendResponse(500, 'Error: Parameter <b>id</b> is missing' );
 
    switch($_GET['model'])
    {
        // Find respective model    
        case 'posts':
            $model = Post::model()->findByPk($_GET['id']);
            break;
        default:
            $this->_sendResponse(501, sprintf(
                'Mode <b>view</b> is not implemented for model <b>%s</b>',
                $_GET['model']) );
            Yii::app()->end();
    }
    // Did we find the requested model? If not, raise an error
    if(is_null($model))
        $this->_sendResponse(404, 'No Item found with id '.$_GET['id']);
    else
        $this->_sendResponse(200, CJSON::encode($model));
}

Create a new Model Action

public function actionCreate()
{
    switch($_GET['model'])
    {
        // Get an instance of the respective model
        case 'posts':
            $model = new Post;                    
            break;
        default:
            $this->_sendResponse(501, 
                sprintf('Mode <b>create</b> is not implemented for model <b>%s</b>',
                $_GET['model']) );
                Yii::app()->end();
    }
    // Try to assign POST values to attributes
    foreach($_POST as $var=>$value) {
        // Does the model have this attribute? If not raise an error
        if($model->hasAttribute($var))
            $model->$var = $value;
        else
            $this->_sendResponse(500, 
                sprintf('Parameter <b>%s</b> is not allowed for model <b>%s</b>', $var,
                $_GET['model']) );
    }
    // Try to save the model
    if($model->save())
        $this->_sendResponse(200, CJSON::encode($model));
    else {
        // Errors occurred
        $msg = "<h1>Error</h1>";
        $msg .= sprintf("Couldn't create model <b>%s</b>", $_GET['model']);
        $msg .= "<ul>";
        foreach($model->errors as $attribute=>$attr_errors) {
            $msg .= "<li>Attribute: $attribute</li>";
            $msg .= "<ul>";
            foreach($attr_errors as $attr_error)
                $msg .= "<li>$attr_error</li>";
            $msg .= "</ul>";
        }
        $msg .= "</ul>";
        $this->_sendResponse(500, $msg );
    }
}

Update a Model Action

public function actionUpdate()
{
    // Parse the PUT parameters. This didn't work: parse_str(file_get_contents('php://input'), $put_vars);
    $json = file_get_contents('php://input'); //$GLOBALS['HTTP_RAW_POST_DATA'] is not preferred: http://www.php.net/manual/en/ini.core.php#ini.always-populate-raw-post-data
    $put_vars = CJSON::decode($json,true);  //true means use associative array
 
    switch($_GET['model'])
    {
        // Find respective model
        case 'posts':
            $model = Post::model()->findByPk($_GET['id']);                    
            break;
        default:
            $this->_sendResponse(501, 
                sprintf( 'Error: Mode <b>update</b> is not implemented for model <b>%s</b>',
                $_GET['model']) );
            Yii::app()->end();
    }
    // Did we find the requested model? If not, raise an error
    if($model === null)
        $this->_sendResponse(400, 
                sprintf("Error: Didn't find any model <b>%s</b> with ID <b>%s</b>.",
                $_GET['model'], $_GET['id']) );
 
    // Try to assign PUT parameters to attributes
    foreach($put_vars as $var=>$value) {
        // Does model have this attribute? If not, raise an error
        if($model->hasAttribute($var))
            $model->$var = $value;
        else {
            $this->_sendResponse(500, 
                sprintf('Parameter <b>%s</b> is not allowed for model <b>%s</b>',
                $var, $_GET['model']) );
        }
    }
    // Try to save the model
    if($model->save())
        $this->_sendResponse(200, CJSON::encode($model));
    else
        // prepare the error $msg
        // see actionCreate
        // ...
        $this->_sendResponse(500, $msg );
}

Please keep in mind to check your model beforeSave and afterSave methods if any code eventually uses a logged-in user's id like the blog Post model:

protected function beforeSave()
{
    ...
    // author_id may have been posted via API POST
    if(is_null($this->author_id) or $this->author_id=='')
        $this->author_id=Yii::app()->user->id;
    ...
}

Delete a Model Action

public function actionDelete()
{
    switch($_GET['model'])
    {
        // Load the respective model
        case 'posts':
            $model = Post::model()->findByPk($_GET['id']);                    
            break;
        default:
            $this->_sendResponse(501, 
                sprintf('Error: Mode <b>delete</b> is not implemented for model <b>%s</b>',
                $_GET['model']) );
            Yii::app()->end();
    }
    // Was a model found? If not, raise an error
    if($model === null)
        $this->_sendResponse(400, 
                sprintf("Error: Didn't find any model <b>%s</b> with ID <b>%s</b>.",
                $_GET['model'], $_GET['id']) );
 
    // Delete the model
    $num = $model->delete();
    if($num>0)
        $this->_sendResponse(200, $num);    //this is the only way to work with backbone
    else
        $this->_sendResponse(500, 
                sprintf("Error: Couldn't delete model <b>%s</b> with ID <b>%s</b>.",
                $_GET['model'], $_GET['id']) );
}

Additional Methods Needed

Sending the Response

How are the API responses actually sent? Right, we need to implement the _sendResponse method.

This code is borrowed from http://www.gen-x-design.com/archives/create-a-rest-api-with-php.

private function _sendResponse($status = 200, $body = '', $content_type = 'text/html')
{
    // set the status
    $status_header = 'HTTP/1.1 ' . $status . ' ' . $this->_getStatusCodeMessage($status);
    header($status_header);
    // and the content type
    header('Content-type: ' . $content_type);
 
    // pages with body are easy
    if($body != '')
    {
        // send the body
        echo $body;
    }
    // we need to create the body if none is passed
    else
    {
        // create some body messages
        $message = '';
 
        // this is purely optional, but makes the pages a little nicer to read
        // for your users.  Since you won't likely send a lot of different status codes,
        // this also shouldn't be too ponderous to maintain
        switch($status)
        {
            case 401:
                $message = 'You must be authorized to view this page.';
                break;
            case 404:
                $message = 'The requested URL ' . $_SERVER['REQUEST_URI'] . ' was not found.';
                break;
            case 500:
                $message = 'The server encountered an error processing your request.';
                break;
            case 501:
                $message = 'The requested method is not implemented.';
                break;
        }
 
        // servers don't always have a signature turned on 
        // (this is an apache directive "ServerSignature On")
        $signature = ($_SERVER['SERVER_SIGNATURE'] == '') ? $_SERVER['SERVER_SOFTWARE'] . ' Server at ' . $_SERVER['SERVER_NAME'] . ' Port ' . $_SERVER['SERVER_PORT'] : $_SERVER['SERVER_SIGNATURE'];
 
        // this should be templated in a real-world solution
        $body = '
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
    <title>' . $status . ' ' . $this->_getStatusCodeMessage($status) . '</title>
</head>
<body>
    <h1>' . $this->_getStatusCodeMessage($status) . '</h1>
    <p>' . $message . '</p>
    <hr />
    <address>' . $signature . '</address>
</body>
</html>';
 
        echo $body;
    }
    Yii::app()->end();
}

Getting the Status Codes

Also, we need to implement the _getStatusCodeMessage method. This is pretty straight forward:

private function _getStatusCodeMessage($status)
{
    // these could be stored in a .ini file and loaded
    // via parse_ini_file()... however, this will suffice
    // for an example
    $codes = Array(
        200 => 'OK',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
    );
    return (isset($codes[$status])) ? $codes[$status] : '';
}

Authentication

If we want to have the API user authorize himself, we could write something like this:

private function _checkAuth()
{
    // Check if we have the USERNAME and PASSWORD HTTP headers set?
    if(!(isset($_SERVER['HTTP_X_USERNAME']) and isset($_SERVER['HTTP_X_PASSWORD']))) {
        // Error: Unauthorized
        $this->_sendResponse(401);
    }
    $username = $_SERVER['HTTP_X_USERNAME'];
    $password = $_SERVER['HTTP_X_PASSWORD'];
    // Find the user
    $user=User::model()->find('LOWER(username)=?',array(strtolower($username)));
    if($user===null) {
        // Error: Unauthorized
        $this->_sendResponse(401, 'Error: User Name is invalid');
    } else if(!$user->validatePassword($password)) {
        // Error: Unauthorized
        $this->_sendResponse(401, 'Error: User Password is invalid');
    }
}

Also, in all REST methods where an authentication is required, we need to put

$this->_checkAuth();

at the beginning of each method.

The API user then needs to set the X_USERNAME and X_PASSWORD headers in his request.

Apache Issues

If PUT or DELETE requests don't work in your Apache setup (perhaps you get an 403 - Forbidden error), you can put the following .htaccess file in the application's web root:

<Limit GET POST PUT DELETE>
order deny,allow
allow from all
</Limit>

See also this link. Other thoughts about mimic PUT and DELETE can be found here.

Discussion

Please add your comments here or send comments to the respective forum post.

Code Download

Of course you can download the code developed here.

Links

Total 20 comments

#16633 report it
malkabani.com at 2014/03/13 06:00am
how display comment

hi all thank you for this artical but what about comment if i want to view post with its comment how it should by

thank you very match

#15902 report it
hemc at 2013/12/28 04:26am
Authentication from request data

_checkAuth() function is authenticating based on the header username and password. If i get authentication data request itself then how can i authenticate a client?

I have request in following format

{
  "request": {
    "signin": {
      "id": "12345",
      "username": "test",
      "password": "xyz"
    },
    "memberid": "03000000015",
    "remarks": { "remark": "check user request remark" }
  }
}

Id,username and password will be validated against db values.

Can you please suggest how to authenticate client in such case?

#15617 report it
lloydzhou at 2013/11/28 11:35pm
another way to build REST api.
  1. create one action JRestAction to handle the RESTful requests.
  2. config this action into controller.
public function actions()
    {
        return array(
            'user'=>array(
                'class' => 'JRestAction',
                'model' => 'User',
        'routes' => array(
            'GET' => array('findAll', 'id' => 'findByPk'),
            'PUT' => array('id' => 'save'),
            'POST' => array('save'),
            'DELETE' => array('id' => 'delete'),
        )
            ),
        );
    }
  1. and then you can test it on url :
    list: GET http://domain.com/controller/user
    view: GET http://domain.com/controller/user/1
    update: PUT http://domain.com/controller/user/1
    create: POST http://domain.com/controller/user
    delete: DELETE http://domain.com/controller/user/1
#15571 report it
Azy at 2013/11/22 05:45am
Performance related,

Hi There,

I am writing my first REST api and would like to get your feedback on this performance matter.

http://www.yiiframework.com/forum/index.php/topic/49119-model-exist-or-not-in-yii-using-a-custom-function-performance/

Appreciate a prompt reply.

Thanks

#15550 report it
whatif at 2013/11/20 07:03pm
Use the right pattern in urlManager

Hi @Azy

I would suggest that you should pay attention to the pattern of urlManager.

The url( http://example.com/RestApi/index.php/api/post/offset/5/limit/10 ) is not matched by any pattern of the urlManager. Hence Yii can not resolve the above url.

To match the above url, the pattern should be , for example, array('api/default/list', 'pattern'=>'api/<model:\w+>/<value1:\w+>/<value2:\d+>/<value3:\w+>/<value4:\d+>', 'verb'=>'GET').

value1 will be set as 'offset', value2 '5', value3 'limit' and value4 '10' by urlManager.

There is an another extension which provides full REST service, which supports sub resources like 'offset/5/limit/10'. This extension is a little complex. But have a look at it if you want to.

http://www.yiiframework.com/extension/restfullyii

Cheers

#15548 report it
Azy at 2013/11/20 01:30pm
URL pattern issue

I have a url pattern defined as below for a REST API build using yii.

'urlManager'=>array( 'urlFormat'=>'path', 'rules'=>array( array('api/default/list', 'pattern'=>'api/<model:\w+>', 'verb'=>'GET'), array('api/default/view', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'GET'), array('api/default/update', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'PUT'), array('api/default/delete', 'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'DELETE'), array('api/default/create', 'pattern'=>'api/<model:\w+>', 'verb'=>'POST'), '<controller:\w+>/<action:\w+>'=>'/', ), ),

What i am trying now is,

http://example.com/RestApi/index.php/api/post?offset=5&limit=10

The above works, but the following doesnt work.

http://example.com/RestApi/index.php/api/post/offset/5/limit/10

How can i make it work like second way? what changes needs to be done to the Url Manager ?

Thanks

Update

The error i get for second way is,

Unable to resolve the request "api/post/offset/5/limit/10".

how do i enable multiple parameters in all put,get,delete/update ? now it only works for one like this,

get - example.com/posts , get - example.com/posts/1 what if i want to add more params like ? example.com/posts/offset/1/limit/10 ?

#15544 report it
Azy at 2013/11/20 05:00am
I resolved it

The issue was a small typo.

i should set the headers this way,

X-Username:azraar X-Password:azraar

instead of underscore(_) have to use a the dash(-)

#15543 report it
whatif at 2013/11/20 04:37am
Read the others' comments about same issue.

Hi @Azy

I cannot know what may be the problem based on your short description.

But see the others' comments, there may be an answer for your case.

Especially read my comment titled 'How to solve "Undefined index error in $_SERVER['HTTP_X_USERNAME']"'

I hope this may help.

#15540 report it
Azy at 2013/11/20 03:24am
HTTP_X_USERNAME & HTTP_X_PASSWORD doesnt set

I am unable to set HTTP_X_USERNAME & HTTP_X_PASSWORD through REST client. Did anyone else get the same problem and/or have a workaround for this ?

Appreciate a quick reply.

#15531 report it
whatif at 2013/11/19 04:26am
Api is controller and View is action

Hi @Azy

According to this REST api document, Api is a controller not a module. And view is an action.

You said you placed Api in module directory as a module, it is a wrong place. Place the ApiController under the protected/controllers/ directory. And actionView as one of the actions in ApiController class.

Cheers.

#15527 report it
Azy at 2013/11/19 03:00am
this doesnt work for me.

i am getting this error when i sent the request this way,

http://localhost/RestApi/index.php/api/post/1

Error 404 Unable to resolve the request "api/view".

i have done this inside a module called "api" and in DefaultController.

#14567 report it
whatif at 2013/08/23 10:14am
How to solve "Undefined index error in $_SERVER['HTTP_X_USERNAME']"

When

$_SERVER['HTTP_X_USERNAME']

is used, the variable name in header should be

X_USERNAME  (without HTTP_ )

since HTTP_ is automatically attached to the variable name in the REST Client extension of Chrome or Firefox browser.

For example, If HTTP_X_USERNAME is used as variable name in header, the index becomes $_SERVER['HTTP_HTTP_X_USERNAME'] therefore "undefined index error in $_SERVER['HTTP_X_USERNAME']" occurs. Beware.

#14334 report it
chamara at 2013/08/05 05:26am
how to get header details?

i'm using rest api for my application to support mobile, i want to know,how i catch the details comes from header?

#13855 report it
Sreenu at 2013/07/02 06:59am
Authentication with Token

Thanks for this tutorial. How to implement "API Authentication with Token" to avoid sending username and password each time? can any one please help on this?

Thanks

#13184 report it
Taufik at 2013/05/12 10:13am
Authorization

If you user Apache+PHP (I tested on XAMPP), maybe you use this :

// Check if we have the USERNAME and PASSWORD HTTP headers set?
    if(!(isset($_SERVER['PHP_AUTH_USER']) and isset($_SERVER['PHP_AUTH_PW']))) {
        // Error: Unauthorized
        $this->_sendResponse(401);
    }
    $username = $_SERVER['PHP_AUTH_USER'];
    $password = $_SERVER['PHP_AUTH_PW'];

instead of

// Check if we have the USERNAME and PASSWORD HTTP headers set?
    if(!(isset($_SERVER['HTTP_X_USERNAME']) and isset($_SERVER['HTTP_X_PASSWORD']))) {
        // Error: Unauthorized
        $this->_sendResponse(401);
    }
    $username = $_SERVER['HTTP_X_USERNAME'];
    $password = $_SERVER['HTTP_X_PASSWORD'];

when use basic authentication (not to add custom header)

#12395 report it
ragulka at 2013/03/18 07:45am
CORS REST API

Hi, I tried to follow TiagoA instructions for the CORS REST API, but I have had no luck. WHen the browser makes the OPTIONS request, it will get a 401 Unauthorized response. Any ideas?

#12197 report it
Sazzad Tushar Khan at 2013/03/05 02:37pm
Last saved id

How can I get the last saved items autoincrement primary key value?

#11828 report it
beesho at 2013/02/06 09:37am
Re: Javascript Authorization

@jwerner Thank you very much!

#11827 report it
jwerner at 2013/02/06 08:22am
Re: Javascript Authorization

@beesho:

Please check this how-to.

Regards, Joachim

#11826 report it
jwerner at 2013/02/06 08:17am
Re: PUT Request

@co-k-ine: The REST Client Requests txt files were just done with the Firefox REST Client Addon and then saved as text files.

It appears as these files just contain a JSON object containing the REST parameters.

Regards, Joachim

Leave a comment

Please to leave your comment.

Write new article