最近API在網絡領域有些風靡,明確的說是REST的影響力。這實在沒什么好驚訝的,因為在任何編程語言中,消費REST API都是非常的容易。構建它也非常的簡單,因為本質上你不會用到任何那些已存在很久的HTTP細則。由於Rails對REST做出的深思熟慮的支持,包括提供和消費這些API(這已經被所有那些和我共事的Rails狂熱者闡述過),我要贊美Rails,這樣的事情並不常發生。
說真的,如果你從未使用過REST,但是使用過(或者更糟糕的,構建過)SOAP API,或僅僅開過一個WSDL並且將你報價單的頭部分解過,伙計,我能有好消息告訴你嗎。
那么,REST到底是什么?為什么你應該關注它?
在開始寫代碼前,我想要確保每個人對REST是什么以及它如何對API有利已經有了較好的認識。首先,從技術上來說,REST並不僅僅針對API,它更像一個通用的概念。然而明顯的是,在這篇文章中,我們將在API的語境下談論REST。所以,讓我們來看看API的基本需求以及REST如何作用它們。
請求
所有的API都需要接受請求。有代表性的,對於一個RESTful API,你會擁有一種定義良好的URL模式。讓我們假設你想要在你的網站上為用戶提供一個API(我知道,我總是為我的例子使用“用戶”的概念)。好的,你的URL結構可能類似於“api/users”和“api/users/[id]”這樣,這取決於針對你的API被請求的操作類型。你還需要考慮要如何接受數據。目前大多數人正在使用JSON或XML,我個人更傾向於JSON,因為它與JavaScript配合使用更好些,而且PHP擁有簡單的功能來編碼和解碼JSON。如果過去你希望你的API真正的穩健,你能夠接受兩者通過嗅探出請求的內容類型(例如:application/json 或 application/xml),但是更讓人接受的是將內容類型限制成一種。真見鬼,如果你願意你甚至可以使用簡單的鍵/值對。
請求的另一塊內容是它實際上做了什么,比如加載,保存等等。一般的,你不得不想出某種體系架構來定義請求者(消費者)請求的是什么動作,但是REST簡化了這些。通過使用HTTP請求方法或動詞,我們不需要定義任何東西。我們能夠僅僅使用GET,POST,PUT和DELETE方法,這些包含了任何我們需要的請求。你可以將這些動詞等價於標准的CRUD風格的東西:GET=加載/檢索,POST=創建,PUT=更新,DELETE=delete。注意到這些動詞不可以直接對應CRUD是重要的,但是這是一種理解它們的好方法。重新回到上面URL的例子,讓我們看一看一些可能的請求意味着什么:
- GET request to /api/users – 列出所有用戶
- GET request to /api/users/1 – 列出ID為1的用戶信息
- POST request to /api/users – 創建一個新用戶
- PUT request to /api/users/1 – 更新ID為1的用戶信息
- DELETE request to /api/users/1 – 刪除ID為1的用戶信息
正如你希望看到的一樣,REST通過一些簡單,易於理解的標准和協議已經處理了很多在構建API時的主要的棘手問題,但對於一個良好的API還有另一塊內容。
響應
因此,REST處理請求非常的容易,但它也容易生成響應。類似於請求,一個RESTful響應有兩個主要的部件:響應主體和狀態碼。響應主體非常容易去處理。像請求一樣,大多數REST上的響應或者是JSON文件或者是XML文件(可能在POST情況下僅僅是一個平面文件,這個我們將在之后提到)。同樣的,像請求一樣,通過另一部分HTTP請求細則—“Accept”,用戶能夠指定他們想要的響應類型。如果用戶希望得到一個XML響應,他們可以僅僅發送一個Accept頭信息“Accept: application/xml”作為請求的一部分。無可否認,這個方法沒有被廣泛的采用,所以你也能在URL中使用擴展的概念。例如,“/api/users.xml”意味着用戶想要XML作為響應,類似的,“ /api/users.json”意味着用戶要響應JSON(“/api/users/1.json/xml”同理)。無論你選擇哪種方式,你都應該選擇一種默認的響應類型,因為大多數情況人們甚至都不會告訴你他們想要的。再次聲明,我會說選擇JSON。如此,沒有Accept頭或擴展(例如:/api/users)也不應該失敗,它應該僅僅以默認的響應類型做“容錯”處理。
但是,錯誤和另外一些重要的與請求相關聯的狀態信息怎么辦呢?這簡單,使用HTTP狀態碼!這已經超過了我對於構建RESTful API的興趣。通過使用HTTP狀態碼,你不需要為你的API想出一種“錯誤/成功”處理模式,這已經被實現了。例如,如果一個用戶用POST方法發送“ /api/users”的請求,並且你想要返回一個成功的產物,簡單的發送一個201狀態碼(201=被創造)就可以了。如果失敗,發送500狀態碼(500=內部服務器錯誤),或者如果搞砸了發送400狀態碼(400=錯誤請求)。可能用戶嘗試用一些不被接受的post去攻擊API端點,發送一個501狀態碼(不被執行)。或許你的MySQL服務器宕機了,因此你的API會被臨時性的凍結,發送一個503狀態碼(服務器不可用)。希望你理解了這個思路。如果你想要閱讀更多關於狀態碼的內容,在wikipedia上查閱它們:List of HTTP Status Codes。
我希望你了解了通過使用REST的概念構建你的API的所有優勢。這真的非常的酷,這沒有在PHP社區被廣泛的討論是一件恥辱的事(至少就我所討論到的)。我認為很大部分原因是缺乏關於如何去處理GET或POST以外,也就是PUT和DELETE方法請求的好的文檔。不可否認,處理它們確實有些蠢,但是這肯定不困難。我非常確認一些流行的框架里面很可能存在某種形式的REST實現,但我並不是一個狂熱的框架迷(基於很多的原因以致於我不想加入),即使有人已經為你實現了解決方案,知道這些對你也是有好處的。
如果你仍然不確信這是一種有用的API范型,看看REST為RoR做了些什么。主要被標榜的一條是構建API將會如何的簡單(通過一些RoR狂熱者,我確信),事實上也確實如此。對於RoR我知之甚少,但是辦公室周圍的那些RoR迷多次的給我指出這點。但是,好吧我離題了,讓我們寫一些代碼!
開始使用REST和PHP
一條最終的免責聲明:我們將要看到的代碼是不可能作為一種穩健的解決方案的例子的。在這里,我的主要目的是向你展示如何在PHP中處理REST的獨立部件,將構建最終解決方案的權利留給你。
讓我們向深處挖掘!我認為對於我們需要創建一個REST API這件事最好的做一些實際有用的事就是創建一個類,這個類將提供所有的工具函數。我們也會創建一個小類用來儲存我們的數據。你也可以拿走它擴展它和在自己的需求中使用它。讓我們貼一些代碼:
1 class RestUtils 2 { 3 public static function processRequest() 4 { 5 6 } 7 8 public static function sendResponse($status = 200, $body = '', $content_type = 'text/html') 9 { 10 11 } 12 13 public static function getStatusCodeMessage($status) 14 { 15 // these could be stored in a .ini file and loaded 16 // via parse_ini_file()... however, this will suffice 17 // for an example 18 $codes = Array( 19 100 => 'Continue', 20 101 => 'Switching Protocols', 21 200 => 'OK', 22 201 => 'Created', 23 202 => 'Accepted', 24 203 => 'Non-Authoritative Information', 25 204 => 'No Content', 26 205 => 'Reset Content', 27 206 => 'Partial Content', 28 300 => 'Multiple Choices', 29 301 => 'Moved Permanently', 30 302 => 'Found', 31 303 => 'See Other', 32 304 => 'Not Modified', 33 305 => 'Use Proxy', 34 306 => '(Unused)', 35 307 => 'Temporary Redirect', 36 400 => 'Bad Request', 37 401 => 'Unauthorized', 38 402 => 'Payment Required', 39 403 => 'Forbidden', 40 404 => 'Not Found', 41 405 => 'Method Not Allowed', 42 406 => 'Not Acceptable', 43 407 => 'Proxy Authentication Required', 44 408 => 'Request Timeout', 45 409 => 'Conflict', 46 410 => 'Gone', 47 411 => 'Length Required', 48 412 => 'Precondition Failed', 49 413 => 'Request Entity Too Large', 50 414 => 'Request-URI Too Long', 51 415 => 'Unsupported Media Type', 52 416 => 'Requested Range Not Satisfiable', 53 417 => 'Expectation Failed', 54 500 => 'Internal Server Error', 55 501 => 'Not Implemented', 56 502 => 'Bad Gateway', 57 503 => 'Service Unavailable', 58 504 => 'Gateway Timeout', 59 505 => 'HTTP Version Not Supported' 60 ); 61 62 return (isset($codes[$status])) ? $codes[$status] : ''; 63 } 64 } 65 66 class RestRequest 67 { 68 private $request_vars; 69 private $data; 70 private $http_accept; 71 private $method; 72 73 public function __construct() 74 { 75 $this->request_vars = array(); 76 $this->data = ''; 77 $this->http_accept = (strpos($_SERVER['HTTP_ACCEPT'], 'json')) ? 'json' : 'xml'; 78 $this->method = 'get'; 79 } 80 81 public function setData($data) 82 { 83 $this->data = $data; 84 } 85 86 public function setMethod($method) 87 { 88 $this->method = $method; 89 } 90 91 public function setRequestVars($request_vars) 92 { 93 $this->request_vars = $request_vars; 94 } 95 96 public function getData() 97 { 98 return $this->data; 99 } 100 101 public function getMethod() 102 { 103 return $this->method; 104 } 105 106 public function getHttpAccept() 107 { 108 return $this->http_accept; 109 } 110 111 public function getRequestVars() 112 { 113 return $this->request_vars; 114 } 115 }
好的,我們已經得到了一個用來保存一些關於請求(REST請求)信息的簡單類,我們可以利用這個類中的一些靜態方法去處理請求和響應。正如你看到的,我們確實僅有兩個方法要寫。這是整件事情最美的地方!好的,讓我們繼續。
處理請求
處理請求是相當直接的,但是在這里我們能遇到一些獵物(即:PUT和DELETE等,多數是PUT)。我們將在某個時刻重溫他們,但現在讓我們檢查下RestRequest類。如果你注意到了構造函數,你就會看到我們已經解釋了HTTP_ACCEPT頭部,如果沒被提供,將默認為JSON。有了這樣的方式,我們只需要處理傳入的數據。
我們有很多的方式去做這個,但是假設我們總是會在請求中得到一個鍵值對:‘數據’=> 實際的數據。同樣假設實際的數據是JSON。在我之前對REST的解釋中,你能夠看到請求的內容類型和或者JSON或者XML的處理方式,但是現在我們應該盡量讓其簡單。因此,我們處理請求的方法最終看起來像這樣:
1 public static function processRequest() 2 { 3 // get our verb 4 $request_method = strtolower($_SERVER['REQUEST_METHOD']); 5 $return_obj = new RestRequest(); 6 // we'll store our data here 7 $data = array(); 8 9 switch ($request_method) 10 { 11 // gets are easy... 12 case 'get': 13 $data = $_GET; 14 break; 15 // so are posts 16 case 'post': 17 $data = $_POST; 18 break; 19 // here's the tricky bit... 20 case 'put': 21 // basically, we read a string from PHP's special input location, 22 // and then parse it out into an array via parse_str... per the PHP docs: 23 // Parses str as if it were the query string passed via a URL and sets 24 // variables in the current scope. 25 parse_str(file_get_contents('php://input'), $put_vars); 26 $data = $put_vars; 27 break; 28 } 29 30 // store the method 31 $return_obj->setMethod($request_method); 32 33 // set the raw data, so we can access it if needed (there may be 34 // other pieces to your requests) 35 $return_obj->setRequestVars($data); 36 37 if(isset($data['data'])) 38 { 39 // translate the JSON to an Object for use however you want 40 $return_obj->setData(json_decode($data['data'])); 41 } 42 return $return_obj; 43 }
就像我說的,非常的直接。然而,有些事情要注意。首先,特別的對於DELETE請求不可以接受數據,因此我們在switch中沒有對應的case。第二點,你會注意到我們儲存了請求變量和解析過的JSON數據這兩者。隨着你可能有另外的東西作為你的請求的一部分(一個API鍵或其他什么東西)時這非常有用,這些東西本身並不是真實的數據(像用戶的名字,郵箱,等等)。
那么,我們如何使用這個呢?讓我們回到用戶例子。假設你已經為用戶將你的請求路由到正確的控制器了,我們會有一些這樣的代碼:
1 $data = RestUtils::processRequest(); 2 3 switch($data->getMethod) 4 { 5 case 'get': 6 // retrieve a list of users 7 break; 8 case 'post': 9 $user = new User(); 10 $user->setFirstName($data->getData()->first_name); // just for example, this should be done cleaner 11 // and so on... 12 $user->save(); 13 break; 14 // etc, etc, etc... 15 }
請不要在真正的應用程序中這樣做,這只是一個應急的例子。你會想把這個封裝在一個任何東西都已被合適抽象的很好的控制結構里,但是這個應該幫助你理解了如何使用這個素材。好吧,我離題了,讓我們進入到發送響應部分。
發送響應
現在我們能解釋請求了,讓我們往前到發送響應部分。我們已經知道實際需要做的是發送正確狀態碼,可能有一些主體(例如,如果是GET請求),但是會有一個重要的捕獲對於那些沒有主體的響應。假如某人用一個不存在的用戶請求攻擊我們簡單地用戶API(如:api/user/123)。在這種情況下,發送404狀態碼是合適的,但是簡單地在頭部里發送狀態碼是不夠的。如果在你的瀏覽器中查看這個頁面,你將會看到空白頁。這是因為Apache(或者其它運行着的Web服務器)沒有發送狀態碼,所以沒有狀態頁面。我們需要考慮這些當構建我們的方法的時候。記住這些,下面是大致的代碼:
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> <p>' . $message . '</p> <hr /> <address>' . $signature . '</address> </body> </html>'; echo $body; exit; } }
這就是了!在技術上我們有了處理請求和發送響應的一切需要的東西。讓我們更多的談談為什么我們需要一個標准的或自定義的響應主體。對於GET請求來說,這是非常明顯的,我們需要發送“XML/JSON”內容取代狀態頁面(只要請求是有效的)。然而,還有POST要處理。在你的應用程序里,當你創建一個新實體,你可能會通過類似mysql_insert_id()函數這樣的方法來獲取新實體的ID。如果一個用戶向你的API發送一個POST請求,他們同樣想要一個新ID。我通常處理這種情況的方法是簡單的發送一個作為主體的新的ID(伴隨一個201狀態碼),但是如果你願意你可以將他們封裝在XML或JSON中。
讓我們稍微來擴展一下我們的簡單應用:
1 switch($data->getMethod) 2 { 3 // this is a request for all users, not one in particular 4 case 'get': 5 $user_list = getUserList(); // assume this returns an array 6 7 if($data->getHttpAccept == 'json') 8 { 9 RestUtils::sendResponse(200, json_encode($user_list), 'application/json'); 10 } 11 else if ($data->getHttpAccept == 'xml') 12 { 13 // using the XML_SERIALIZER Pear Package 14 $options = array 15 ( 16 'indent' => ' ', 17 'addDecl' => false, 18 'rootName' => $fc->getAction(), 19 XML_SERIALIZER_OPTION_RETURN_RESULT => true 20 ); 21 $serializer = new XML_Serializer($options); 22 23 RestUtils::sendResponse(200, $serializer->serialize($user_list), 'application/xml'); 24 } 25 26 break; 27 // new user create 28 case 'post': 29 $user = new User(); 30 $user->setFirstName($data->getData()->first_name); // just for example, this should be done cleaner 31 // and so on... 32 $user->save(); 33 34 // just send the new ID as the body 35 RestUtils::sendResponse(201, $user->getId()); 36 break; 37 }
重申一下,這僅僅是一個例子,但是它展示了(我認為,至少是這樣的)實現RESTful所要做出的努力。
總結
這就是了!我非常自信已經把一些觀點易於理解的指了出來,因此我願意接受你如何更進一步的領悟這個材料,而且或許可以正確的實現它。
在現實的MVC應用程序中,你或許想做的是為你的加載個別API控制器的API設置一個控制器。例如,使用上面的原型,我們可能創建一個包含get(),put(),post()和delete()方法的UserRestController控制器。這些方法將會使用工具來處理請求,智能的做一些需要做的事,然后使用工具發送響應。
你也可以更進一步,抽象出你的API控制器和數據模型。不用在你的應用程序中為每個數據模型創建一個控制器,你可以添加一些邏輯到你的API控制器並且首先尋找一個顯示定義的控制器,如果沒找到,則嘗試去尋找一個存在的模型。例如,URL“api/user/1”將會首先查找一個“user”的rest控制器,如果沒找到,再在你的應用程序中尋找一個叫做“user”的模型。如果找到了一個,你可以對這些模型寫一些自動化巫師程序來自動化處理所有的請求。
更進一步,你可以創建一個一般的“list-all”方法,其工作原理類似於先前段落的例子。假設你的url是“api/users”。API控制器將會首先檢查“users”rest控制器,如果沒找到,識別用戶被多元化,解除多元化,然后查找“user”模型。如果發現一個,加載一個列表的用戶列表並發出。
最后,你可以同樣簡單的為你的API加上摘要式身份驗證。假設你只想要合適認證的用戶有訪問你API的權限,你可以向這樣在你的處理請求的功能在加入一些代碼(借用我的現有應用,有一些常量和變量引用沒有被定義在這個片段中)。
1 // figure out if we need to challenge the user 2 if(emptyempty($_SERVER['PHP_AUTH_DIGEST'])) 3 { 4 header('HTTP/1.1 401 Unauthorized'); 5 header('WWW-Authenticate: Digest realm="' . AUTH_REALM . '",qop="auth",nonce="' . uniqid() . '",opaque="' . md5(AUTH_REALM) . '"'); 6 7 // show the error if they hit cancel 8 die(RestControllerLib::error(401, true)); 9 } 10 11 // now, analayze the PHP_AUTH_DIGEST var 12 if(!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || $auth_username != $data['username']) 13 { 14 // show the error due to bad auth 15 die(RestUtils::sendResponse(401)); 16 } 17 18 // so far, everything's good, let's now check the response a bit more... 19 $A1 = md5($data['username'] . ':' . AUTH_REALM . ':' . $auth_pass); 20 $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']); 21 $valid_response = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2); 22 23 // last check.. 24 if($data['response'] != $valid_response) 25 { 26 die(RestUtils::sendResponse(401)); 27 }
非常酷的原型,是嗎?通過一點點代碼和一些聰明的邏輯,你可以非常快捷的在你的應用程序中加入一個全功能的REST API。我並不僅僅是在鼓勵這個概念,我花了半天時間在我的個人框架中實現了它,又花了另一個半天在里面加入了各式各樣的魔法。如果你對我的最終實現感興趣,在評論中注明,我將非常高興地與你分享。如果你有任何願意分享的酷的點子,同樣也請在評論中寫下它們。如果我足夠喜歡它,我甚至樂意讓您自己創作關於這一主題的文章。
下次再見。