This article will explain how to create a REST API with the Yii framework.
A good introduction about implementing REST service with PHP can be found on http://www.gen-x-design.com.
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.
We will create an API that allows us to
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!
When using the API, we would like to have the following URL scheme:
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.
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() { } }
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)); } }
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)); }
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 ); } }
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; ... }
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']) ); }
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(); }
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] : ''; }
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.
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.
Please add your comments here or send comments to the respective forum post.
Of course you can download the code developed here.
Total 20 comments
If you user Apache+PHP (I tested on XAMPP), maybe you use this :
instead of
when use basic authentication (not to add custom header)
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?
How can I get the last saved items autoincrement primary key value?
@jwerner Thank you very much!
@beesho:
Please check this how-to.
Regards, Joachim
@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
How I can test put request with the demo application ? (In folder "rest client request" put request is missing).
Would anyone please guide me on how to do Ajax/Javascript authorization to use this API? I spent a lot of time getting it to work. Am new to Yii. Here is what am trying:
Thanks in advance.
There are multiple XSS problems in your script. A htmlspecialchars() on all user-input would fix it.
@Johny Joe: I was wondering exactly the same thing!
That's why I used a 2 step process:
To authenticate the user I used the amazon method so no password is transiting. basically it calculate the hmac of a known string with the user password as the secret key: http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
The known string is the concat of multiple arguments: expiration time of the request, url requested, ...
I want to get list of posts ,what I have to write,to get it from rest service.
This are mentioned above in comments. $username = $SERVER['HTTP_X'.self::APPLICATION_ID.'USERNAME']; $password = $_SERVER['HTTP_X'.self::APPLICATION_ID.'_PASSWORD'];
How to set this variable through cURL ?
I am not understanding how it is working? can u please describe how the client side working? the demo contains the API controller but is it used anywhere in the demo?
Just a reference of REST specifications at the following link:
RESTful Web services
in case there are future editors of this wiki :)
Great post. Thanks. I am implementing this for our site Your text to link here... . BTW, I think that this can be encapsulated in a separated module.
Thanks for this, i am using it for a web app at the moment.
actionDelete
actionUpdate
Thanks for this tutorial but how can one show certain column only example title description etc because when I run actionlist I get all column with their respective but want to show only values from certain column as I have mentioned. Thanks again.
Thanks for this great tutorial!
Just an idea but wouldn't it be nicer to throw CHttpExceptions in situations like when a model wasn't found instead of calling sendResponse()? It should be possible to overwrite the errorHandler component and the actionError() in the ApiController to take the error codes and messages like this
cheers,
Hannes
Leave a comment
Please login to leave your comment.