用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