原文链接 :http://www.yiiframework.com/wiki/175/how-to-create-a-rest-api/
这篇文章将给大家解释,如何用Yii framework创建一个REST API
与REST相关的信息
关于如何用PHP实现REST服务,http://www.gen-x-design.com. 上面有一个非常好的介绍,
一些工具
To fire up a REST request to a Yii application(这句不知怎么翻译妥当),你可以用Firefox REST Client Addon.
如果你想通过console来发送REST请求,你也许可以参照一下cUrl
Requirements 需求
我们要创建一个API来满足以下需求:
- Get 指定model的所有items
- Get 一个 item 通过model 的 primary key
- Create a new item
- Update an existing item
- Delete an existing item.
在这个教程里面,将以Yii Blog demo为例子. 这里讲的model指的是Post model, 例如,creating and reading post items。
这个API将会非常灵活,可以很轻松地扩展使得可以在其它的models上面工作.例如,comments, 或者 user data
所有的对API的requests都要通过authorization.
All right, 我们开动吧!
设置 URL Manager
当用到API的时候,我们可能会看到下面形式的URL:
- View all posts: index.php/api/posts (HTTP method GET)
- View 一个 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)
为了解析这些URL,在config/main.php中,如下,设置URL manager
... '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>', ), ), ...
注意,对于所有的requests,我们将get the requested model(例如posts) 通过 GET 名为model 的 parameter
对于,Get Single Item and Update Item 方法, 我们将通过GET的id参数接收model's primary key
Create an API controller
在这个教程中, 我们将在一个new controller中实现所有的RESTS方法. 请把这个文件放在controllers文件夹中
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() { } }
实现这些Actions
·获取所有的models
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)); } }
单独获取一个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 '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一个新的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 ); } }
Update a Model
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 ); }
请注意检查你beforeSave
和 afterSave
methods,如果你的代码当中使用了已登陆的用户的id, 例如下面一样,这时author_id 应该通过POST参数获取。
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']) ); }
其它methods
发送Response
API responses是实际上是如何发送的? 对, 我们需要实现 _sendResponse method.
下面的代码是从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(); }
获取 Status Codes
当然, 我们也需要实现 _getStatusCodeMessage method. 下面实现的方法相当直接。
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
如果,我们想有对API使用者的验证功能,我们需要写如下的代码:
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'); } }
同时,在所有的需要被验证的REST methods当中 ,我们需要在method的最开始的部门放置如下代码
$this->_checkAuth();
如此API使用者,需要在request 中设置X_USERNAME and X_PASSWORD .
Apache 的问题
如果在Apache中 PUT 或者 DELETE requests (也许你遇到了403 - Forbidden error 的问题),你可以放置 .htaccess 文件在你web应用的根目录当中
<Limit GET POST PUT DELETE> order deny,allow allow from all </Limit>
同时可以参看 this link. 有关模仿 PUT 和DELETE的其它观点可以在这里这里找到 .
讨论
请在forum post添加你的评论 .
代码下载
当然你可以下载上面说到的代码 here.