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
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
Cheers
Thanks for the tutorial.
I think the HTTP Status Codes are used wrongly in some places:
[list]
[*] 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.
http://localhost/yii/demos/blog/index.php/api/posts
Error 404
The system is unable to find the requested action "posts".
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
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.
//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'),
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.
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
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