New Yii REST Tutorial

Hello,

I have written a tutorial about how to implement a REST API with Yii::

http://www.yiiframework.com/wiki/175/how-to-create-a-rest-api/

Please post your feedback here!

Best Regards,

Joachim

Nice job. Based on some comments in the article, it may need some tweaking, but nice to see someone that thought out the process and wrote it down :slight_smile:

Cheers

Thanks for the tutorial.

I think the HTTP Status Codes are used wrongly in some places:

  • actionList

[list]

  • if list is not implemented for a model I would rather give a 400 Invalid Request, instead of 501; for the latter the specs (RFC2616) say:

[*] actionView

  • if the necessary ‘id’ parameter is missing, it should be a 400 Invalid Request

  • for the model not implementing the action, see above

[*] actionCreate

  • for the model not implementing the action, see above

  • if a parameter is not allowed, 400 Invalid Request

[*] actionUpdate

  • for the model not implementing the action, see above

  • if the model for update cant be found, its 404 Not Found

  • if a parameter is not allowed, 400 Invalid Request

[*] actionDelete

  • for the model not implementing the action, see above

  • if the model for delete cant be found, its 404 Not Found

[/list]

can you explain whats the purpose of REST? I read some tutorial but seems can’t understand it.

bryglen

It’s about exposing application functionality via API.

nice, needed it just about now. Thanks :)

Joachim,

I want to create the api so a java program can access data.

Following your tutorial

* 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)

should i point them in the browser to test or in the restful client.

  1. in the browser i am getting the following error.

http://localhost/yii/demos/blog/index.php/api/posts

Error 404

The system is unable to find the requested action "posts".

  1. in the RESTFUL client firefox plugin

pressed login button

selected basic

username X_USERNAME

password X_PASSWORD

Method Get

url sent: http://localhost/yii/demos/blog/index.php/api/posts/

i get the same 404 error

  1. Please help
  1. does your api controller have a function called actionPosts ?

nope.




<?php


/**


 * ApiController class file


 * @author Joachim Werner <joachim.werner@diggin-data.de>  


 */


/**


 * ApiController 


 * 


 * @uses Controller


 * @author Joachim Werner <joachim.werner@diggin-data.de>


 * @author 


 * @see http://www.gen-x-design.com/archives/making-restful-requests-in-php/


 * @license (tbd)


 */


class ApiController extends Controller


{


    // {{{ *** Members ***


    /**


     * Key which has to be in HTTP USERNAME and PASSWORD headers 


     */


    Const APPLICATION_ID = 'ASCCPE';





    private $format = 'json';


    // }}} 


    // {{{ filters


    /**


     * @return array action filters


     */


    public function filters()


    {


            return array();


    } // }}} 


    // {{{ *** Actions ***


    // {{{ actionIndex


    public function actionIndex()


    {


        echo CJSON::encode(array(1, 2, 3));


    } // }}} 


    // {{{ actionList


    public function actionList()


    {


        $this->_checkAuth();


        switch($_GET['model'])


        {


            case 'posts': // {{{ 


                $models = Post::model()->findAll();


                break; // }}} 


            default: // {{{ 


                $this->_sendResponse(501, sprintf('Error: Mode <b>list</b> is not implemented for model <b>%s</b>',$_GET['model']) );


                exit; // }}} 


        }


        if(is_null($models)) {


            $this->_sendResponse(200, sprintf('No items where found for model <b>%s</b>', $_GET['model']) );


        } else {


            $rows = array();


            foreach($models as $model)


                $rows[] = $model->attributes;





            $this->_sendResponse(200, CJSON::encode($rows));


        }


    } // }}} 


    // {{{ actionView


    /* Shows a single item


     * 


     * @access public


     * @return void


     */


    public function actionView()


    {


        $this->_checkAuth();


        // 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']) );


                exit; // }}} 


        }


        if(is_null($model)) {


            $this->_sendResponse(404, 'No Item found with id '.$_GET['id']);


        } else {


            $this->_sendResponse(200, $this->_getObjectEncoded($_GET['model'], $model->attributes));


        }


    } // }}} 


    // {{{ actionCreate


    /**


     * Creates a new item


     * 


     * @access public


     * @return void


     */


    public function actionCreate()


    {


        $this->_checkAuth();





        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']) );


                exit; // }}} 


        }


        // Try to assign POST values to attributes


        foreach($_POST as $var=>$value) {


            // Does the model have this attribute?


            if($model->hasAttribute($var)) {


                $model->$var = $value;


            } else {


                // No, raise an error


                $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()) {


            // Saving was OK


            $this->_sendResponse(200, $this->_getObjectEncoded($_GET['model'], $model->attributes) );


        } 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 );


        }





        var_dump($_REQUEST);


    } // }}}     


    // {{{ actionUpdate


    /**


     * Update a single iten


     * 


     * @access public


     * @return void


     */


    public function actionUpdate()


    {


        $this->_checkAuth();





        // Get PUT parameters


        parse_str(file_get_contents('php://input'), $put_vars);





        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']) );


                exit; // }}} 


        }


        if(is_null($model))


            $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($model->hasAttribute($var)) {


                $model->$var = $value;


            } else {


                // No, raise error


                $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, sprintf('The model <b>%s</b> with id <b>%s</b> has been updated.', $_GET['model'], $_GET['id']) );


        } else {


            $msg = "<h1>Error</h1>";


            $msg .= sprintf("Couldn't update 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 );


        }


    } // }}} 


    // {{{ actionDelete


    /**


     * Deletes a single item


     * 


     * @access public


     * @return void


     */


    public function actionDelete()


    {


        $this->_checkAuth();





        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']) );


                exit; // }}} 


        }


        // Was a model found?


        if(is_null($model)) {


            // No, raise an error


            $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, sprintf("Model <b>%s</b> with ID <b>%s</b> has been deleted.",$_GET['model'], $_GET['id']) );


        else


            $this->_sendResponse(500, sprintf("Error: Couldn't delete model <b>%s</b> with ID <b>%s</b>.",$_GET['model'], $_GET['id']) );


    } // }}} 


    // }}} End Actions


    // {{{ Other Methods


    // {{{ _sendResponse


    /**


     * Sends the API response 


     * 


     * @param int $status 


     * @param string $body 


     * @param string $content_type 


     * @access private


     * @return void


     */


    private function _sendResponse($status = 200, $body = '', $content_type = 'text/html')


    {


        $status_header = 'HTTP/1.1 ' . $status . ' ' . $this->_getStatusCodeMessage($status);


        // set the status


        header($status_header);


        // set 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 = '';





            // 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 templatized 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;


        }


    } // }}}            


    // {{{ _getStatusCodeMessage


    /**


     * Gets the message for a status code


     * 


     * @param mixed $status 


     * @access private


     * @return string


     */


    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(


            100 => 'Continue',


            101 => 'Switching Protocols',


            200 => 'OK',


            201 => 'Created',


            202 => 'Accepted',


            203 => 'Non-Authoritative Information',


            204 => 'No Content',


            205 => 'Reset Content',


            206 => 'Partial Content',


            300 => 'Multiple Choices',


            301 => 'Moved Permanently',


            302 => 'Found',


            303 => 'See Other',


            304 => 'Not Modified',


            305 => 'Use Proxy',


            306 => '(Unused)',


            307 => 'Temporary Redirect',


            400 => 'Bad Request',


            401 => 'Unauthorized',


            402 => 'Payment Required',


            403 => 'Forbidden',


            404 => 'Not Found',


            405 => 'Method Not Allowed',


            406 => 'Not Acceptable',


            407 => 'Proxy Authentication Required',


            408 => 'Request Timeout',


            409 => 'Conflict',


            410 => 'Gone',


            411 => 'Length Required',


            412 => 'Precondition Failed',


            413 => 'Request Entity Too Large',


            414 => 'Request-URI Too Long',


            415 => 'Unsupported Media Type',


            416 => 'Requested Range Not Satisfiable',


            417 => 'Expectation Failed',


            500 => 'Internal Server Error',


            501 => 'Not Implemented',


            502 => 'Bad Gateway',


            503 => 'Service Unavailable',


            504 => 'Gateway Timeout',


            505 => 'HTTP Version Not Supported'


        );





        return (isset($codes[$status])) ? $codes[$status] : '';


    } // }}} 


    // {{{ _checkAuth


    /**


     * Checks if a request is authorized


     * 


     * @access private


     * @return void


     */


    private function _checkAuth()


    {


        // Check if we have the USERNAME and PASSWORD HTTP headers set?


        if(!(isset($_SERVER['HTTP_X_'.self::APPLICATION_ID.'_USERNAME']) and isset($_SERVER['HTTP_X_'.self::APPLICATION_ID.'_PASSWORD']))) {


            // Error: Unauthorized


            $this->_sendResponse(401);


        }


        $username = $_SERVER['HTTP_X_'.self::APPLICATION_ID.'_USERNAME'];


        $password = $_SERVER['HTTP_X_'.self::APPLICATION_ID.'_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');


        }


    } // }}} 


    // {{{ _getObjectEncoded


    /**


     * Returns the json or xml encoded array


     * 


     * @param mixed $model 


     * @param mixed $array Data to be encoded


     * @access private


     * @return void


     */


    private function _getObjectEncoded($model, $array)


    {


        if(isset($_GET['format']))


            $this->format = $_GET['format'];





        if($this->format=='json')


        {


            return CJSON::encode($array);


        }


        elseif($this->format=='xml')


        {


            $result = '<?xml version="1.0">';


            $result .= "\n<$model>\n";


            foreach($array as $key=>$value)


                $result .= "    <$key>".utf8_encode($value)."</$key>\n"; 


            $result .= '</'.$model.'>';


            return $result;


        }


        else


        {


            return;


        }


    } // }}} 


    // }}} End Other Methods


}





/* vim:set ai sw=4 sts=4 et fdm=marker fdc=4: */


?>



Oh, I see.

In that case you probably forgot to include the action:

http://example.com/api/view/posts

Also check your URL rules!

Yii is convinced that ‘posts’ is an action, when it’s really a get parameter.

jacmoe,

i really do not want to rewrite the post view, either that’s already there with the demo/blog or in the rewrite of REST tutorial. my problem is not that page is not displayed by a particular url (../index.php/api/posts or whatever)

../blog/index.php/api/

correctly displays 123

for the index action of api controller




 public function actionIndex()


    {


        echo CJSON::encode(array(1, 2, 3));


    } // }}} 



my question is how are the other actions, i.e. actionView, actionCreate, actionUpdate supposed to work for a REST client or even in a browser.

Well they are supposed to do the anticipated api functions just for the purpose they were created. right.

Thanks in anticipation

Check that you are using a GET, POST, PUT or DELETE request. :)

You can use the rest client Firefox addon for testing.

First up; thanks. This was a big help to me. I’ve got a nice system running on a heavily modified version (many controllers, handling models based on db VIEWS, etc).

My problem: I simply can’t UPDATE. I’m using pretty much vanilla version of “Making RESTful requests in PHP” (see below), alas PHP’s buffer appears empty. The following code logs literally nothing (an empty string):




public function actionUpdate() {

...

$foo = file_get_contents('php://input');

$log = new LogTable;

$log->log = $foo;

$log->save();

...

}

Any ideas? I’m assuming “Making REST requests” code is right; it looks it.

  • I don’t get any errors from Apache, and none from PHP.

  • Both the server and client are on the same machine.

  • I’ve “allowed” PUT in .htaccess (overrides are allowed)

  • I’ve modified executePut in the client to have:


curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');

when before it had:


curl_setopt($ch, CURLOPT_PUT, true);

As you can imagine, I’m starting to lose it a bit. Scenes from Fight Club keep flashing before my eyes!

Any help appreciated. Thanks!

Links: www.gen-x-design.com/archives/making-restful-requests-in-php/

Incidentally:

Apache access log says:

127.0.0.1 - - [20/Jul/2011:17:48:19 +0100] "PUT /yiirest/index.php/api/people/1 HTTP/1.1" 200 58 "-" "-"

I’ve put the following at the very beginning of /yiirest/index.php (i.e. before we even load Yii):

$foo = (file_get_contents(‘php://input’));

$fh = fopen(‘test.txt’, ‘w’);

fwrite($fh, $foo);

fclose($fh);

test.txt is zero length :(

And furthermore, if I print curl_error($ch) after execution, this happens:

With


curl_setopt($ch, CURLOPT_PUT, TRUE);

I get no error from curl_error($ch) but a 413 from the API server

With


curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');

I get a curl error Operation timed out after 10000 milliseconds with 0 bytes received

I’ve now exhausted my limited knowledge of CURL bah humbug!

I have just implemented this Tutorial and I wanted to add some information for those who have run into tangentially related issues.

  1. When using UUIDs in implementing the urlManager rules in this example, you need to modify the pattern as such (update the regex):



//OLD:

// 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'),


//NEW:

// REST patterns

array('api/list', 'pattern'=>'api/<model:\w+>', 'verb'=>'GET'),

array('api/view', 'pattern'=>'api/<model:\w+>/<id:[\w-]+>', 'verb'=>'GET'),

array('api/update', 'pattern'=>'api/<model:\w+>/<id:[\w-]+>', 'verb'=>'PUT'),

array('api/delete', 'pattern'=>'api/<model:\w+>/<id:[\w-]+>', 'verb'=>'DELETE'),

array('api/create', 'pattern'=>'api/<model:\w+>', 'verb'=>'POST'),



  1. The tutorial advises to use REST Client for Firefox. Using the software, you need to manually add the the Request Header. I needed to have Content-Type set to application/x-www-form-urlencoded. Not doing this will result in PHP not registering the $_POST variable and will break the create functionality (because it cycles through $_POST). As a new user to Yii, I assumed it was due to something in the framework. It’s not, it’s due to PHP.

  2. The create functionality needs _getObjectEncoded implemented.

Hi savage1881,

I’m trying to use Rest extension for firefox for this tutorial, but I cant seem to get it to work for actionCreate.

I tried adding the application/x-www-form-urlencoded as you have stated in your comment. Still not working :slight_smile:

Are you using this example with JSON, and if so, what exactly do you put in the firefox extension to get it to work?

Thanks in advance!

Cheers, JJ

realy nice jobs Thanks for this Information…

Hi all,

I’m still stuck with using the firefox REST-client. It seems that Yii thinks the $_POST variable is always empty, no matter what i put in the extension.

Any help is greatly appreciated!

Cheers, JJ

Add the following information to your Request Header:


Content-Type: application/x-www-form-urlencoded 

Using the button on top (add Request header, ‘Content-Type’ is the ‘name’ and ‘application/x-www-form-urlencoded’ the ‘value’. Then you can put information like:




variable_1=ValueOf1stVariable&variable_2=ValueOf2ndVariable 

in your request body.

Thanks! Finally got it working.

Cheers, JJ