用PHP建立一個REST APi

認真的講,假如你歷來沒有使用過REST,卻曾經使用過SOAP API,或者只是簡單的打開一個使人頭大的WSDL文檔。小夥子,我確實要帶給你一個好消息! javascript

 

那麼,究竟什麼是REST?爲何你應該關心? php

在咱們開始寫代碼以前,我想要確認每一個人均可以很好的理解什麼是REST以及它是如何特別適合APIs的。首先,從技術上來說,REST並非僅僅特定於APIs應用,它更多的是一個通用的概念。然而,很明顯,咱們這篇文章所討論的REST就是在接口應用的環境下。所以,讓咱們看看一個API的基本要求已經REST如何處理他們。 html

Requests 請求 java

全部的APIs都須要接收請求。對於一個RESTful API,你須要一個定義好的URL規則,咱們假定你想要提供一個接口給你網站上的用戶(我知道,我老是使用"用戶"這個概念來舉例)。你的URL結構可能相似於這樣:"api/users"或者"api/users/[id]",這取決於請求接口的操做類型。同時,你也須要考慮你想要怎麼樣接收數據。近來一段時間,不少人正在使用JSON或者XML,從我我的來說,我更加喜歡JSON,由於它能夠很好的和javascript進行交互操做,同時PHP也能夠很簡單的經過json_encode和json_decode兩個函數來對它進行編碼和解碼。若是你但願本身的接口真正強健,你應該經過識別請求的內容類型(好比application/json或者application/xml)同時容許接收兩種格式。可是,限制只接收一種類型的數據也是能夠很好的被接受。真見鬼,假如你願意,你甚至可使用簡單的鍵/值對。 mysql

一個請求的其餘部分是它真正要作的事情,好比加載、保存等。一般來講,你應該提供幾種結構來定義請求者(消費者)所但願的操做,可是REST簡化了這些。經過使用HTTP請求方法或者動做,咱們不須要去額外定義任何東西。咱們能夠僅僅使用GET,POST,PUT和DELETE方法,這些方法涵蓋了咱們所須要的每個請求。你能夠把它和標準的增刪改查模式對應起來:GET=加載/檢索(查,select),POST=建立(增,Create),PUT=更新(改,update),DELETE=刪除(DELETE)。咱們要注意到,這些動詞並無直接翻譯成CRUD(增刪改查),可是這個理解它們的一個很好的方法。所以,回到剛纔所舉的URL的例子,讓咱們看一下一些可能的請求的含義: git

    GET request to /api/users – 列舉出全部的用戶 sql

    GET request to /api/users/1 – 列出ID爲1的用戶信息 apache

    POST request to /api/users – 插入一個新的用戶 json

    PUT request to /api/users/1 – 更新ID爲1的用戶信息 api

    DELETE request to /api/users/1 – 刪除ID爲1的用戶 

正如你所但願看到的,REST已經解決了不少使人頭疼的建立接口的問題,經過一些簡單的,容易理解的標準和協議。可是一個好的接口還要另一個方面... 

Responses 響應 

因此,REST能夠很簡單的處理請求,同時它也能夠簡單的處理響應。和請求相似,一個RESTful的響應主要包括兩個主要部分:響應體和狀態碼。響應體很是容易去處理。就像請求,大部分的REST響應一般是JSON或者XML格式(也許對POST操做來講僅僅是純文本,咱們稍後會討論),和請求相似,消費者能夠經過設置HTTP規範的"Accept"選項來規定本身作但願接收到的響應數據類型。若是一個消費者但願接收到XML響應,他們僅僅須要發送一個包含相似於(」Accept: application/xml」)這樣的頭信息請求。不能否認,這種方式並無被普遍的採用(即便應該這樣),所以你也可使用URL後綴的形式,例如:/api/users.xml意味着消費者但願獲得XML響應,一樣,/api/users.json意味着JSON格式的響應(/api/users/1.json/xml也是同樣)。無論你採用哪種方法,你都須要設定一個默認的響應類型,由於不少時候人們並不會告訴你他們但願什麼格式。再次地,我會選擇JSON來討論。因此,沒有Accept頭信息或者擴展(例如:/api/users)不該該失敗,而是採用默認的響應類型。 

可是和請求有關的錯誤和其餘重要的狀態信息怎麼辦呢?簡單,使用HTTP的狀態碼!這是我建立RESTful接口最喜歡的事情之一。經過使用HTTP狀態碼,你不須要爲你的接口想出error/success規則,它已經爲你作好。好比:假如一個消費者提交數據(POST)到/api/users,你須要返回一個成功建立的消息,此時你能夠簡單的發送一個201狀態碼(201=Created)。若是失敗了,服務器端失敗就發送一個500(500=內部服務器錯誤),若是請求中斷就發送一個400(400=錯誤請求)。也許他們會嘗試向一個不接受POST請求的接口提交數據,你就能夠發送一個501錯誤(未執行)。又或者你的MySQL服務器掛了,接口也會臨時性的中斷,發送一個503錯誤(服務不可用)。幸運的是,你已經知道了這些,假如你想要了解更多關於狀態碼的資料,能夠在維基百科上查找:List of HTTP Status Codes。 

我但願你能看到REST接口的這些優勢。它真的超級酷。在PHP社區社區裏沒有被普遍的討論真是很是的遺憾(至少我知道的是這樣)。我以爲這主要是因爲沒有很好的文檔介紹如何處理除了GET和POST以後的請求,即PUT和DELETE。不能否認,處理這些是有點傻,可是卻不難。我相信一些流行的框架也許已經有了某種REST的實現方式,可是我不是一個框架粉絲(緣由有不少),而且即便有人已經爲你提供瞭解決方案,你知道這些也是很是有好處的。 

若是你仍是不太自信這是一個很是有用的API範式,看一下REST已經爲Ruby on Rails作了什麼。其中最使人稱道的就是建立接口的便利性(經過某種RoR voodoo,我確信),並且確實如此。雖然我對RoR瞭解不多,可是辦公室的Ruby粉絲們向我說教過不少次。很差意思跑題了,讓咱們開始寫代碼。 

 

Getting Started with REST and PHP 開始使用PHP寫REST 

最後一項免責聲明:咱們接下來提供的代碼並不能被用來做爲一個穩健的解決方案。個人主要目的是向你們展現若是使用PHP處理REST的每一個單獨部分,而把最後的解決方案留給大家本身去建立。 

那麼,讓咱們開始深刻代碼。我認爲作一個實際事情作好的方法就是新建一個class,這個class將提供建立REST API所須要的全部功能性方法。如今咱們新建一個小的class來存儲咱們的數據。你能夠把它拿去擴展一下而後應用到本身的需求中。咱們如今開始寫點東西: 

 

Php代碼  收藏代碼

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()函數來解析出來,然而這樣也足夠了,好比:  

        $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;  

    }  

}  

好,如今咱們有了一個簡單的class來存儲request的一些信息(RestRequest),和一個提供幾個靜態方法的class來處理請求和響應。就像你能看到的,咱們還有兩個方法要去寫,這纔是整個代碼的關鍵所在,讓咱們繼續... 

處理請求的過程很是直接,可是這纔是咱們能夠有所收穫的地方(即PUT/DELETE,大多數是PUT),咱們接下來將會討論這些。可是讓咱們先來檢查一下RestRequest這個class,在構造方法中,你會看到咱們已經處理了HTTP_ACCEPT的頭信息,而且將JSON做爲默認值。這樣,咱們就只須要處理傳入的數據。 

咱們有幾個方法能夠選擇,可是讓咱們假設在請求信息的老是能夠接收到鍵/值對:'data'=>真實數據。同時假設真實數據是JSON格式的。正如我前文所述,你能夠根據請求的內容類型來處理JSON或者XML,可是讓咱們如今簡單一點。那麼,咱們處理請求的方法將會相似於這樣: 

 

Php代碼  收藏代碼

public static function processRequest(){  

    // get our verb 獲取動做  

    $request_method = strtolower($_SERVER['REQUEST_METHOD']);  

    $return_obj     = new RestRequest();  

    // we'll store our data here 在這裏存儲請求數據  

    $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;  

}  

Lie 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條件。其次,你會注意到咱們把請求參數和解析後的JSON數據都存儲起來了,這在請求中有其餘須要處理的數據時會變得很是有用(API key或者其餘),這些並非請求的數據自己(好比一個新用戶的名字、電子郵箱等)。 

那麼,咱們如何使用它呢?讓咱們回到剛纔user的例子。假設你已經經過路由把請求對應到正確的users控制器,代碼以下: 

Php代碼  收藏代碼

$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...  

}  

請不要在真實的應用中這樣作,這是一個很是快速和不乾淨的示例。你應該使用一個設計良好的控制結構來把它包裹起來,適當的抽象化,可是這樣有助於你理解如何使用這些東西。讓咱們繼續代碼,發送一個響應信息。 

既然咱們已經能夠解析請求,那麼接下來咱們繼續來發送一個響應。咱們已經知道咱們真正須要去作的是發送一個正確的狀態碼和一些響應消息體(例如這是一個GET請求),可是對於沒有消息體的響應來講有一個重要的catch(譯者:很差意思,實在是不知道如何翻譯這個詞)。假定某我的向咱們的user接口發送一個請求某個用戶信息的請求,而這個用戶卻不存在(好比:api/user/123),此時系統發送最合適的狀態碼是404。可是簡單的在頭信息中發送狀態碼是不夠的,若是你經過網頁瀏覽器瀏覽該頁面,你會看到一個空白頁面。這是由於apache服務器(或者其餘服務器)並不會發送此狀態碼,所以沒有狀態頁面。咱們須要在構建方法的時候考慮到這一點。把全部的東西都考慮進去,代碼會相似於下面這樣: 

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  

       

    }  

}  

就這樣,從技術上來講,咱們已經具有了處理請求和發送響應的全部東西。下面咱們再討論如下爲何咱們須要一個標準的相應提或者一個自定義的。對於GET請求來講,很是明顯,咱們須要發送XML/JSON內容而不是一個狀態頁(假設請求是合法的)。而後,咱們還有POST請求要去處理。在你的應用內部,當你建立一個新的實體,你也許須要使用經過相似於mysql_insert_id()這樣的函數獲得這個實體的ID。那麼,當一個用戶提交到你的接口,他們將極可能想要知道這個新的ID是什麼。在這種狀況下,我一般的作法是很是簡單的把這個新ID做爲響應的消息體發送給用戶(同時發送一個201的狀態碼頭信息),可是若是你願意,你也可使用XML或者JSON來把它包裹起來。 

如今,讓咱們來擴展一下咱們的例子,讓它更加實際一點: 

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;  

}  

再一次說明,這是一個例子,但它確實向咱們展現了(至少我認爲是)它能垂手可得的實現RESTful接口。 

因此,這就是它。我很是的自信的說,我已經把這些解釋的很是清楚。所以,我就再也不贅述你如何具體實現它。 

在一個真實的MVC應用中,也許你想要作的就是爲你的每一個接口建立一個單獨的控制器。例如,利用上面的東西,咱們能夠建立一個UserRestController控制器,這個控制器有四個方法,分別爲:get(), put(), post(), 和 delete()。接口控制器將會查看請求類型而後決定哪一個方法會被執行。這個方法會再使用工具來處理請求,處理數據,而後使用工具發送響應。 

你也許會比如今更進一步,把你的接口控制器和數據模型抽象出來,而不是明確的爲每個數據模型建立控制器,你能夠給你的接口控制器添加一些邏輯,先去查找一個明肯定義好的控制器,若是沒有,試着去查找一個已經存在的模型。例如:網址"api/user/1"將會首先觸發查找一個叫user的最終控制器,若是沒有,它會查找應用中叫user的模型,若是找到了,你能夠寫一個自動化的方法來自動處理全部請求這個模型的請求。 

再進一步,你能夠創建一個通用的"list-all"方法,就像上面一段中的例子同樣。假定你的url是"api/usrs",接口控制器首先會查找叫users的控制器,若是沒有找到,確認users是複數,把它變成單數,而後查找一個叫user的模型,若是找到了,加載一個用戶列表而後把他們發送出去。 

最後,你能夠給你的接口添加簡單的身份驗證。假定你僅僅但願適當的驗證訪問你的接口的用戶,那麼,你能夠在處理請求的方法中添加相似於下面的一些代碼(借用個人一個現有應用,所以有一些常量和變量在這個代碼片斷裏面並無被定義): 

Php代碼  收藏代碼

// figure out if we need to challenge the user  

if(emptyempty($_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));  

}  

很是酷,對吧?經過少許的代碼和一些智能的邏輯,你能夠很是快速的給你的應用添加全功能的REST接口。我並不只僅是支持這個概念,我已經在我我的的框架裏面實現了這些東西,而這些僅僅花費了半天的時間,而後再花費半天時間添加一些很是酷的東西。若是你(讀者)對我最終的實現感興趣,請在評論中留言,我會很是樂趣和你分享它。同時,若是你有什麼比較酷的想法,也歡迎經過評論和我進行分享。若是我足夠喜歡它,我會邀請你在這裏發表本身的文章。 

http://git.oschina.net/anziguoer/restAPI

相關文章
相關標籤/搜索