class RestUtils{ public static function processRequest(){ } public static function sendResponse($status = 200, $body = '', $content_type = 'text/html'){ } public static function getStatusCodeMessage($status){ // these could be stored in a .ini file and loaded // via parse_ini_file()... however, this will suffice // for an example // 這些應該被存儲在一個.ini的文件中,然后通過parse_ini_file()函數(shù)來解析出來,然而這樣也足夠了,比如: $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] : ''; }}class RestRequest{ private $request_vars; private $data; private $http_accept; private $method; public function __construct(){ $this->request_vars = array(); $this->data = ''; $this->http_accept = (strpos($_SERVER['HTTP_ACCEPT'], 'json')) ? 'json' : 'xml'; $this->method = 'get'; } public function setData($data){ $this->data = $data; } public function setMethod($method){ $this->method = $method; } public function setRequestVars($request_vars){ $this->request_vars = $request_vars; } public function getData(){ return $this->data; } public function getMethod(){ return $this->method; } public function getHttpAccept(){ return $this->http_accept; } public function getRequestVars(){ return $this->request_vars; }}
OK, so what we’ve got is a simple class for storing some information about our request (RestRequest), and a class with some static functions we can use to deal with requests and responses. As you can see, we really only have two functions to write… which is the beauty of this whole thing! Right, let’s move on…
Processing the Request
好,現(xiàn)在我們有了一個簡單的class來存儲request的一些信息(RestRequest),和一個提供幾個靜態(tài)方法的class來處理請求和響應。就像你能看到的,我們還有兩個方法要去寫,這才是整個代碼的關鍵所在,讓我們繼續(xù)...
Processing the request is pretty straight-forward, but this is where we can run into a few catches (namely with PUT and DELETE… mostly PUT). We’ll Go over those in a moment, but let’s examine the RestRequest class a bit. If you’ll look at the constructor, you’ll see that we’re already interpreting the HTTP_ACCEPT header, and defaulting to JSON if none is provided. With that out of the way, we need only deal with the incoming data.
處理請求的過程非常直接,但是這才是我們可以有所收獲的地方(即 PUT/DELETE,大多數(shù)是PUT),我們接下來將會討論這些。但是讓我們先來檢查一下RestRequest這個class,在構(gòu)造方法中,你會看 到我們已經(jīng)處理了HTTP_ACCEPT的頭信息,并且將JSON作為默認值。這樣,我們就只需要處理傳入的數(shù)據(jù)。
There are a few ways we could go about doing this, but let’s just assume that we’ll always get a key/value pair in our request: ‘data’ => actual data. Let’s also assume that the actual data will be JSON. As stated in my previous explanation of REST, you could look at the content-type of the request and deal with either JSON or XML, but let’s keep it simple for now. So, our process request function will end up looking something like this:
我們有幾個方法可以選擇,但是讓我們假設在請求信息的總是可以接收到鍵/值 對:'data'=>真實數(shù)據(jù)。同時假設真實數(shù)據(jù)是JSON格式的。正如我前文所述,你可以根據(jù)請求的內(nèi)容類型來處理JSON或者XML,但是讓我 們現(xiàn)在簡單一點。那么,我們處理請求的方法將會類似于這樣:
public static function processRequest(){ // get our verb 獲取動作 $request_method = strtolower($_SERVER['REQUEST_METHOD']); $return_obj = new RestRequest(); // we'll store our data here 在這里存儲請求數(shù)據(jù) $data = array(); switch ($request_method){ // gets are easy... case 'get': $data = $_GET; break; // so are posts case 'post': $data = $_POST; break; // here's the tricky bit... case 'put': // basically, we read a string from PHP's special input location, // and then parse it out into an array via parse_str... per the PHP docs: // Parses str as if it were the query string passed via a URL and sets // variables in the current scope. parse_str(file_get_contents('php://input'), $put_vars); $data = $put_vars; break; } // store the method $return_obj->setMethod($request_method); // set the raw data, so we can access it if needed (there may be // other pieces to your requests) $return_obj->setRequestVars($data); if(isset($data['data'])){ // translate the JSON to an Object for use however you want $return_obj->setData(json_decode($data['data'])); } return $return_obj;}
Like I said, pretty straight-forward. However, a few things to note… First, you typically don’t accept data for DELETE requests, so we don’t have a case for them in the switch. Second, you’ll notice that we store both the request variables, and the parsed JSON data. This is useful as you may have other stuff as a part of your request (say an API key or something) that isn’t truly the data itself (like a new user’s name, email, etc.).
正如我剛才所說的,非常的簡單直接高效。然后,有幾點需要注意:首先,我們不接受 DELETE請求,因此我們在switch中不提供相應的case條件。其次,你會注意到我們把請求參數(shù)和解析后的JSON數(shù)據(jù)都存儲起來了,這在請求中 有其他需要處理的數(shù)據(jù)時會變得非常有用(API key或者其他),這些并不是請求的數(shù)據(jù)本身(比如一個新用戶的名字、電子郵箱等)。
So, how would we use this? Let’s go back to the user example. Assuming you’ve routed your request to the correct controller for users, we could have some code like this:
那么,我們?nèi)绾问褂盟??讓我們回到剛才user的例子。假設你已經(jīng)通過路由把請求對應到正確的users控制器,代碼如下:
$data = RestUtils::processRequest();switch($data->getMethod){ case 'get': // retrieve a list of users break; case 'post': $user = new User(); $user->setFirstName($data->getData()->first_name); // just for example, this should be done cleaner // and so on... $user->save(); break; // etc, etc, etc...}
Please don’t do this in a real app, this is just a quick-and-dirty example. You’d want to wrap this up in a nice control structure with everything abstracted properly, but this should help you get an idea of how to use this stuff. But I digress, let’s move on to sending a response.
Sending the Response
請不要在真實的應用中這樣做,這是一個非??焖俸筒桓蓛舻氖纠D銘撌褂靡粋€設計良好的控制結(jié)構(gòu)來把它包裹起來,適當?shù)某橄蠡?,但是這樣有助于你理解如何使用這些東西。讓我們繼續(xù)代碼,發(fā)送一個響應信息。
Now that we can interpret the request, let’s move on to sending the response. We already know that all we really need to do is send the correct status code, and maybe some body (if this were a GET request, for example), but there is an important catch to responses that have no body. Say somebody made a request against our sample user API for a user that doesn’t exist (i.e. api/user/123). The appropriate status code to send is a 404 in this case, but simply sending the status code in the headers isn’t enough. If you viewed that page in your web browser, you would get a blank screen. This is because Apache (or whatever your web server runs on) isn’t sending the status code, so there’s no status page. We’ll need to take this into account when we build out our function. Keeping all that in mind, here’s what the code should look like:
既然我們已經(jīng)可以解析請求,那么接下來我們繼續(xù)來發(fā)送一個響應。我們已經(jīng)知道我們真正 需要去做的是發(fā)送一個正確的狀態(tài)碼和一些響應消息體(例如這是一個GET請求),但是對于沒有消息體的響應來說有一個重要的catch(譯者:不好意思, 實在是不知道如何翻譯這個詞)。假定某個人向我們的user接口發(fā)送一個請求某個用戶信息的請求,而這個用戶卻不存在(比如:api/user /123),此時系統(tǒng)發(fā)送最合適的狀態(tài)碼是404。但是簡單的在頭信息中發(fā)送狀態(tài)碼是不夠的,如果你通過網(wǎng)頁瀏覽器瀏覽該頁面,你會看到一個空白頁面。這 是因為apache服務器(或者其他服務器)并不會發(fā)送此狀態(tài)碼,因此沒有狀態(tài)頁面。我們需要在構(gòu)建方法的時候考慮到這一點。把所有的東西都考慮進去,代 碼會類似于下面這樣:
public static function sendResponse($status = 200, $body = '', $content_type = 'text/html'){ $status_header = 'HTTP/1.1 ' . $status . ' ' . RestUtils::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 . ' ' . RestUtils::getStatusCodeMessage($status) . '</title> </head> <body> <h1>' . RestUtils::getStatusCodeMessage($status) . '</h1> ' . $message . ' <hr /> <address>' . $signature . '</address> </body> </html>'; echo $body; exit; }}
That’s It! We technically have everything we need now to process requests and send responses. Let’s talk a bit more about why we need to have a standard body response or a custom one. For GET requests, this is pretty obvious, we need to send XML / JSON content instead of a status page (provided the request was valid). However, there’s also POSTs to deal with. Inside of your apps, when you create a new entity, you probably fetch the new entity’s ID via something like mysql_insert_id(). Well, if a user posts to your API, they’ll probably want that new ID as well. What I’ll usually do in this case is simply send the new ID as the body (with a 201 status code), but you could also wrap that in XML or JSON if you’d like.
就這樣,從技術上來說,我們已經(jīng)具備了處理請求和發(fā)送響應的所有東西。下面我們再討論 以下為什么我們需要一個標準的相應提或者一個自定義的。對于GET請求來說,非常明顯,我們需要發(fā)送XML/JSON內(nèi)容而不是一個狀態(tài)頁(假設請求是合 法的)。然后,我們還有POST請求要去處理。在你的應用內(nèi)部,當你創(chuàng)建一個新的實體,你也許需要使用通過類似于mysql_insert_id()這樣 的函數(shù)得到這個實體的ID。那么,當一個用戶提交到你的接口,他們將很可能想要知道這個新的ID是什么。在這種情況下,我通常的做法是非常簡單的把這個新 ID作為響應的消息體發(fā)送給用戶(同時發(fā)送一個201的狀態(tài)碼頭信息),但是如果你愿意,你也可以使用XML或者JSON來把它包裹起來。
So, let’s extend our sample implementation a bit:
現(xiàn)在,讓我們來擴展一下我們的例子,讓它更加實際一點:
switch($data->getMethod){ // this is a request for all users, not one in particular case 'get': $user_list = getUserList(); // assume this returns an array if($data->getHttpAccept == 'json'){ RestUtils::sendResponse(200, json_encode($user_list), 'application/json'); }else if ($data->getHttpAccept == 'xml') { // using the XML_SERIALIZER Pear Package $options = array ( 'indent' => ' ', 'addDecl' => false, 'rootName' => $fc->getAction(), XML_SERIALIZER_OPTION_RETURN_RESULT => true ); $serializer = new XML_Serializer($options); RestUtils::sendResponse(200, $serializer->serialize($user_list), 'application/xml'); } break; // new user create case 'post': $user = new User(); $user->setFirstName($data->getData()->first_name); // just for example, this should be done cleaner // and so on... $user->save(); // just send the new ID as the body RestUtils::sendResponse(201, $user->getId()); break;}
Again, this is just an example, but it does show off (I think, at least) how little effort it takes to implement RESTfulstuff.
Wrapping Up
再一次說明,這是一個例子,但它確實向我們展示了(至少我認為是)它能輕而易舉的實現(xiàn)RESTful接口。
So, that’s about it. I’m pretty confident that I’ve beaten the point that this should be quite easy into the ground, so I’d like to close with how you can take this stuff further and perhaps properly implement it.
所以,這就是它。我非常的自信的說,我已經(jīng)把這些解釋的非常清楚。因此,我就不再贅述你如何具體實現(xiàn)它。
In a real-world MVC application, what you would probably want to do is set up a controller for your API that loads individual API controllers. For example, using the above stuff, we’d possibly create a UserRestController which had four methods: get(), put(), post(), and delete(). The API controller would look at the request and determine which method to invoke on that controller. That method would then use the utils to process the request, do what it needs to do data-wise, then use the utils to send a response.
在一個真實的MVC應用中,也許你想要做的就是為你的每個接口創(chuàng)建一個單獨的控制器。 例如,利用上面的東西,我們可以創(chuàng)建一個UserRestController控制器,這個控制器有四個方法,分別為:get(), put(), post(), 和 delete()。接口控制器將會查看請求類型然后決定哪個方法會被執(zhí)行。這個方法會再使用工具來處理請求,處理數(shù)據(jù),然后使用工具發(fā)送響應。
You could also take it a step further than that, and abstract out your API controller and data models a bit more. Rather than explicitly creating a controller for every data model in your app, you could add some logic into your API controller to first look for an explicitly defined controller, and if none is found, try to look for an existing model. For example, the url “api/user/1″, would first trigger a lookup for a “user” rest controller. If none is found, it could then look for a model called “user” in your app. If one is found, you could write up a bit of automated voodoo to automatically process all the requests against those models.
你也許會比現(xiàn)在更進一步,把你的接口控制器和數(shù)據(jù)模型抽象出來,而不是明確的為每一個 數(shù)據(jù)模型創(chuàng)建控制器,你可以給你的接口控制器添加一些邏輯,先去查找一個明確定義好的控制器,如果沒有,試著去查找一個已經(jīng)存在的模型。例如:網(wǎng) 址"api/user/1"將會首先觸發(fā)查找一個叫user的最終控制器,如果沒有,它會查找應用中叫user的模型,如果找到了,你可以寫一個自動化的 方法來自動處理所有請求這個模型的請求。
Going even further, you could then make a generic “l(fā)ist-all” method that works similar to the previous paragraph’s example. Say your url was “api/users”. The API controller could first check for a “users” rest controller, and if none was found, recognize that users is pluaralized, depluralize it, and then look for a “user” model. If one’s found, load a list the list of users and send that off.
再進一步,你可以建立一個通用的"list-all"方法,就像上面一段中的例子一 樣。假定你的url是"api/usrs",接口控制器首先會查找叫users的控制器,如果沒有找到,確認users是復數(shù),把它變成單數(shù),然后查找一 個叫user的模型,如果找到了,加載一個用戶列表然后把他們發(fā)送出去。
Finally, you could add digest authentication to your API quite easily as well. Say you only wanted properly authenticated users to access your API, well, you could throw some code like this into your process request functionality (borrowed from an existing app of mine, so there’s some constants and variables referenced that aren’t defined in this snippet):
最后,你可以給你的接口添加簡單的身份驗證。假定你僅僅希望適當?shù)尿炞C訪問你的接口的用戶,那么,你可以在處理請求的方法中添加類似于下面的一些代碼(借用我的一個現(xiàn)有應用,因此有一些常量和變量在這個代碼片段里面并沒有被定義):
// figure out if we need to challenge the user if(empty($_SERVER['PHP_AUTH_DIGEST'])) { header('HTTP/1.1 401 Unauthorized'); header('WWW-Authenticate: Digest realm="' . AUTH_REALM . '",qop="auth",nonce="' . uniqid() . '",opaque="' . md5(AUTH_REALM) . '"'); // show the error if they hit cancel die(RestControllerLib::error(401, true)); } // now, analayze the PHP_AUTH_DIGEST var if(!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || $auth_username != $data['username']) { // show the error due to bad auth die(RestUtils::sendResponse(401)); } // so far, everything's good, let's now check the response a bit more... $A1 = md5($data['username'] . ':' . AUTH_REALM . ':' . $auth_pass); $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']); $valid_response = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2); // last check.. if($data['response'] != $valid_response) { die(RestUtils::sendResponse(401)); }
Pretty cool stuff, huh? With a little bit of code and some clever logic, you can add a fully functional REST API to your apps very quickly. I’m not just saying that to cheerlead the concept either, I implemented this stuff into one of my personal frameworks in about half a day, and then spent another half day adding all sorts of cool magic to it. If you (the reader) are interested in seeing my final implementation, drop me a note in the comments and I’d be happy to share it with you! Also, if you’ve got any cool ideas you’d like to share, be sure to drop those in the comments as well… if I like it enough, I’d even let you guest author your own article on the subject!
非常酷,對吧?通過少量的代碼和一些智能的邏輯,你可以非??焖俚慕o你的應用添加全功 能的REST接口。我并不僅僅是支持這個概念,我已經(jīng)在我個人的框架里面實現(xiàn)了這些東西,而這些僅僅花費了半天的時間,然后再花費半天時間添加一些非???的東西。如果你(讀者)對我最終的實現(xiàn)感興趣,請在評論中留言,我會非常樂趣和你分享它。同時,如果你有什么比較酷的想法,也歡迎通過評論和我進行分享。 如果我足夠喜歡它,我會邀請你在這里發(fā)表自己的文章。
Until next time…
聯(lián)系客服