Difference between #11 and #12 of How-To: Create a REST API

unchanged
Title
How-To: Create a REST API
unchanged
Category
How-tos
unchanged
Tags
REST, API, Tutorial
changed
Content
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](http://www.gen-x-design.com/archives/create-a-rest-api-with-php/).

## Usefull Tools

To fire up a REST request to a Yii application, you can use the [Firefox REST
Client Addon](https://github.com/chao/RESTClient).

If you want to send REST requests via a console, perhaps you might check
[cUrl](http://curl.haxx.se/).


## 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_ (_DELETE_)


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

~~~
[php]
...
'urlManager'=>array(
    'urlFormat'=>'path',
    'rules'=>array(
        'post/<id:\d+>/<title:.*?>'=>'post/view',
        'posts/<tag:.*?>'=>'post/index',
        //        ...
        'urlManager'=>array(
        	'urlFormat'=>'path',
        	'rules'=>array(
                        'post/<id:\d+>/<title:.*?>'=>'post/view',
                        'posts/<tag:.*?>'=>'post/index',
                        // REST patterns
        array('api/list',
                        array('api/list',
'pattern'=>'api/<model:\w+>', 'verb'=>'GET'),
        array('api/view',
                        array('api/view',
'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'GET'),
        array('api/update',
                        array('api/update',
'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'PUT'),
        array('api/delete',  // Update
                        array('api/delete',
'pattern'=>'api/<model:\w+>/<id:\d+>', 'verb'=>'DELETE'),
        array('api/create',
                        array('api/create',
'pattern'=>'api/<model:\w+>', 'verb'=>'POST'),
        // // Create
                        // Other controllers
       
'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
    ),
),
...
                       
'<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:

~~~
[php]
class
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

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


### Get a Single Model Action

~~~
[php]
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'])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
            case 'posts':
            $model
                $model = Post::model()->findByPk($_GET['id']);
            break;
        default:
            $this->_sendResponse(501, sprintf(
                'Mode
                break;
            default:
                $this->_sendResponse(501, sprintf('Mode
<b>view</b> is not implemented for model
<b>%s</b>',
               
$_GET['model'])<b>%s</b>',$_GET['model']) );
            exit;
    }
    //
                exit; // }}} 
        }
        // Did we find the requested model? If not,
        if(is_null($model)) {
            // No, raise an error
    if(is_null($model))
        $this->_sendResponse(404, 
            $this->_sendResponse(404, 'No Item found with id
'.$_GET['id']);
    else
        $this->_sendResponse(200,
        } else {
            
            $this->_sendResponse(200,
CJSON::encode($_GET['model']));
}
        }
    } // }}} 
~~~


### Create a new Model Action

~~~
[php]
public    public function actionCreate()
{
    switch($_GET['model'])
    {
        //switch($_GET['model'])
        {
            // Get an instance of the respective model
        case
            case 'posts':
            $model
                $model = new Post;                    
            break;
        default:
            $this->_sendResponse(501, 
                sprintf('Mode
                break;
            default:
                $this->_sendResponse(501, sprintf('Mode
<b>create</b> is not implemented for model
<b>%s</b>',
               
$_GET['model'])<b>%s</b>',$_GET['model']) );
                exit;
    }
    //    }
        // Try to assign POST values to attributes
    foreach($_POST
        foreach($_POST as $var=>$value) {
        //
            // Does the model have this attribute? If not
            if($model->hasAttribute($var)) {
                $model->$var = $value;
            } else {
                // No, raise an error
        if($model->hasAttribute($var))
            $model->$var = $value;
        else
            $this->_sendResponse(500, 
                sprintf('Parameter
                $this->_sendResponse(500, sprintf('Parameter
<b>%s</b> is not allowed for model <b>%s</b>',
$var,
                $_GET['model']) $_GET['model']) );
    }
    //
            }
        }
        // Try to save the model
    if($model->save())
        $this->_sendResponse(200, 
                $this->_getObjectEncoded($_GET['model'],
        if($model->save()) {
            // Saving was OK
            $this->_sendResponse(200,
$this->_getObjectEncoded($_GET['model'], $model->attributes) );
    else
        } else {
        //
            // Errors occurred
        $msg
            $msg = "<h1>Error</h1>";
        $msg
            $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_errorsforeach($model->errors
as $attr_error)$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 );
        }
        $msg .= "</ul>";
        $this->_sendResponse(500, $msg );
    }
}} // }}}
~~~


### Update a Model Action

~~~
[php]
public    public function actionUpdate()
{
    // Parse the PUT parameters
    parse_str(file_get_contents('php://input'), $put_vars);

    switch($_GET['model'])
    {
        //
        // Parse the PUT parameters
        parse_str(file_get_contents('php://input'), $put_vars);

        switch($_GET['model'])
        {
            // Find respective model
        case
            case 'posts':
            $model
                $model = Post::model()->findByPk($_GET['id']);         
          
            break;
        default:
            $this->_sendResponse(501, 
                sprintf( 'Error:
                break;
            default:
                $this->_sendResponse(501, sprintf('Error: Mode
<b>update</b> is not implemented for model
<b>%s</b>',
               
$_GET['model'])<b>%s</b>',$_GET['model']) );
            exit;
    }
    //
                exit;
        }
        // Did we find the requested model? If not,
        if(is_null($model)) {
            // No, raise an error
    if(is_null($model))
        $this->_sendResponse(400, 
                sprintf("Error:
            $this->_sendResponse(400, sprintf("Error: Didn't find
any model <b>%s</b> with ID <b>%s</b>.",
               
$_GET['model'],<b>%s</b>.",$_GET['model'],
$_GET['id']) );
        }
        
    //    // Try to assign PUT parameters to
attributes
    foreach($put_vars
        foreach($put_vars as $var=>$value) {
        //
            // Does model have this attribute? If not, raise an error
        if($model->hasAttribute($var))
            $model->$var
            if($model->hasAttribute($var)) {
                $model->$var = $value;
        else
            } else {
            $this->_sendResponse(500, 
                sprintf('Parameter
                // No, raise error
                $this->_sendResponse(500, sprintf('Parameter
<b>%s</b> is not allowed for model <b>%s</b>',
                $var, $var, $_GET['model']) );
            }
        }
        // Try to save the model
        if($model->save()) {
            $this->_sendResponse(200, sprintf('The model
<b>%s</b> with id <b>%s</b> has been updated.',
$_GET['model'], $_GET['id']) );
        } else {
            // prepare the error $msg
            // see actionCreate
            // ...
            $this->_sendResponse(500, $msg );
        }
    }
    // Try to save the model
    if($model->save())
        $this->_sendResponse(200, 
                sprintf('The model <b>%s</b> with id
<b>%s</b> has been updated.',
                $_GET['model'], $_GET['id']) );
    else
        // prepare the error $msg
        // see actionCreate
        // ...
        $this->_sendResponse(500, $msg );
}
~~~


### Delete a Model Action

~~~
[php]
public    public function actionDelete()
{
    switch($_GET['model'])
    {
        //switch($_GET['model'])
        {
            // Load the respective model
        case
            case 'posts':
            $model
                $model = Post::model()->findByPk($_GET['id']);         
          
            break;
        default:
            $this->_sendResponse(501, 
                sprintf('Error:
                break;
            default:
                $this->_sendResponse(501, sprintf('Error: Mode
<b>delete</b> is not implemented for model
<b>%s</b>',
               
$_GET['model'])<b>%s</b>',$_GET['model']) );
            exit;
    }
    //
                exit;
        }
        // Was a model found? If not,
        if(is_null($model)) {
            // No, raise an error
    if(is_null($model))
        $this->_sendResponse(400, 
                sprintf("Error:
            $this->_sendResponse(400, sprintf("Error: Didn't find
any model <b>%s</b> with ID <b>%s</b>.",
               
$_GET['model'],<b>%s</b>.",$_GET['model'],
$_GET['id']) );
        }

    //    // Delete the model
    $num
        $num = $model->delete();
    if($num>0)
        $this->_sendResponse(200, 
                sprintf("Model
        if($num>0)
            $this->_sendResponse(200, sprintf("Model
<b>%s</b> with ID <b>%s</b> has been
deleted.",
                $_GET['model'],deleted.",$_GET['model'],
$_GET['id']) );
    else
        $this->_sendResponse(500, 
                sprintf("Error:
        else
            $this->_sendResponse(500, sprintf("Error: Couldn't
delete model <b>%s</b> with ID <b>%s</b>.",
               
$_GET['model'],<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](http://www.gen-x-design.com/archives/create-a-rest-api-with-php).

~~~
[php]
private    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;
        exit;
    }
    // we need to create the body if none is passed
    else
    {
        // create some body messages
        $message$status_header = '';'HTTP/1.1 '
. $status . ' ' . $this->_getStatusCodeMessage($status);
        // set the status
        header($status_header);
        // set the content type
        header('Content-type: ' . $content_type);

        // 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)with body are easy
        if($body != '')
        {
            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;// send the body
            echo $body;
            exit;
        }
        // we need to create the body if none is passed
        else
        {
            // create some body messages
            $message = '';

        // servers don't always have a signature turned on 
        // (this    // this is an apache directive
"ServerSignature On")
        $signaturepurely 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 = ($_SERVER['SERVER_SIGNATURE'] == '') ?
$_SERVER['SERVER_SOFTWARE'] . ' Server at'You must be authorized to
view this page.';
                    break;
                case 404:
                    $message = 'The requested URL ' .
$_SERVER['SERVER_NAME']$_SERVER['REQUEST_URI'] . '
Port ' . $_SERVER['SERVER_PORT'] :
$_SERVER['SERVER_SIGNATURE'];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;
            }

        // this should be templated in    // servers don't
always have a real-world solution
        $bodysignature turned on (this is an apache directive
"ServerSignature On")
            $signature = ($_SERVER['SERVER_SIGNATURE'] == '') ?
$_SERVER['SERVER_SOFTWARE'] . ' Server at '
<!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$_SERVER['SERVER_NAME']
. ' Port ' . $this->_getStatusCodeMessage($status) .
'</title>
</head>
<body>
    <h1>' . $this->_getStatusCodeMessage($status) . '</h1>
    <p>' . $message . '</p>
    <hr />
    <address>' . $signature . '</address>
</body>
</html>';$_SERVER['SERVER_PORT'] :
$_SERVER['SERVER_SIGNATURE'];

        echo    // 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;
        exit;
            exit;
        }
    }
} // }}}            
~~~


### Getting the Status Codes

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

~~~
[php]
private    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
        $codes = Array(
        200
            200 => 'OK',
        400
            400 => 'Bad Request',
        401
            401 => 'Unauthorized',
        402
            402 => 'Payment Required',
        403
            403 => 'Forbidden',
        404
            404 => 'Not Found',
        500
            500 => 'Internal Server Error',
        501
            501 => 'Not Implemented',
    );
    return
        );
        return (isset($codes[$status])) ? $codes[$status] : '';
}
    } // }}} 

~~~


### Authentication

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

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

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

~~~
[php]
    $this->_checkAuth();    $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](http://www.jfileupload.com/support/forums/viewtopic.php?f=2&t=361).
Other thoughts about mimic PUT and DELETE can be found
[here](http://stackoverflow.com/questions/2081894/handling-put-delete-arguments-in-php).


## Discussion

Please add your comments here or send comments to the respective [forum
post](http://www.yiiframework.com/forum/index.php?/topic/18412-new-yii-rest-tutorial/).


## Code Download

Of course you can download the code developed
[here](http://www.diggin-data.de/download/yii-blog-rest.tar.gz).