本文作者:心月

如何把 SOAP API 改造成符合RESTful 風格的API

心月IT博客 2019-04-24
摘要:SOAP(簡單對象訪問協議)和REST(Representational State Transfer)都是Web服務的通信協議。SOAP長期以來一直是Web服務接口的標準方法,近年來它開始由REST主導,根據Stormpath統計,有超過70%的公共API使用 REST API 。

    SOAP(簡單對象訪問協議)和REST(Representational State Transfer)都是Web服務的通信協議。SOAP長期以來一直是Web服務接口的標準方法,近年來它開始由REST主導,根據Stormpath統計,有超過70%的公共API使用 REST API 。


    現在有超過一半的API 使用的是REST API, 是否可以把 SOAP API 改造成 REST API,如果可以要怎么做呢?


    RESTful架構風格規定,數據的元操作,即CRUD(create, read, update和delete,即數據的增刪查改)操作,分別對應于HTTP方法:GET用來獲取資源,POST用來新建資源(也可以用于更新資源),PUT用來更新資源,DELETE用來刪除資源,這樣就統一了數據操作的接口,僅通過HTTP方法,就可以完成對數據的所有增刪查改工作。


    因此,把 SOAP API 改造成符合RESTful 風格的 API 主要要改的就是把SOAP中的CURD操作與 HTTP 方法對應起來。


SOAP API 的功能:

●注冊用戶
●創建文章
●修改文章
●刪除文章
●獲取單篇文章
●獲取文章列表


服務器環境:Apache+mysql

項目host:http://api.restful.com/

改造步驟:

RESTful API 是單一路口,因此首先需要處理好訪問路徑跳轉處理。

    在項目根目錄下新建一個 restful 目錄作為 REST API 訪問路口,在 restful 目錄下新建 index.php 作為接口的唯一入口,所有的操作都通過這里完成,再新建一個.htaccess文件做重定向處理,重定向規則如下:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [L]

重定向規則的意思:

    當 http://api.restful.com/restufl/ 后接的目錄或文件不存在時跳轉到 restful 下的 index.php,并且,/restful/ 后接的內容做為參數交給 index.php 處理。


    例如:http://api.restful.com/restful/users,當請求這個路徑時實際請求的是http://api.restful.com/restful/index.php,而  users 就成了需要處理的參數。


入口處理好后就可以開始改造了,改造 SOAP 不需要動它原有的代碼,在 restful 目錄下新建 ResfulLphp 類文件,創建 Restful 類

class Restful
{
    //用戶資源
    private $_user;

    //文章資源
    private $_article;

    //請求的方法
    private $_requestMethod;

    //請求的資源名稱
    private $_resourceName;

    //允許請求的資源ID
    private $_id;

    //允許請求的資源列表
    private $_allowResources = ['users','articles'];

    //允許請求的http方法
    private $_allowRequestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];

    //常用狀態碼
    private $_statusCodes = [
        200 => 'OK',
        204 => 'No Content',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allow',
        500 => 'Server Internal Error'
    ];

在 SOAP 中有 user 和 article 兩類資源,因此在資源屬性以及資源列表中都只有 user 和 article ,如果請求的不是這兩類資源請求將不會被允許。

●GET: 對應單篇文章請求 和 文章列表請求
●POST: 對應創建文章 和 注冊用戶
●PUT: 對應編輯文章
●DELETE: 對應刪除文章

①construct初始化

在 Restful 類中只有兩個公有方法,初始化以及 API 請求

public function __construct(User $_user, Article $_article)
{
    $this->_user = $_user;
    $this->_article = $_article;
}

public function run()
{
    try{
        $this->_setupRequestMethod();
        $this->_setupResource();
        if ($this->_resourceName == 'users'){
            return $this->_handleUser();
        }else{
            return $this->_handleArticle();
        }
    }catch (Exception $e){
        return $this->_json(['message'=>$e->getMessage()], $e->getCode());
    }
}

//輸出json
private function _json($array, $code)
{
    if ($array === null && $code === 0){
        $code = 204;
    }
    if ($array !== null && $code === 0){
        $code = 200;
    }

    header('HTTP/1.1 '.$code.' '.$this->_statusCodes[$code]);
    header('Content-Type:application/json;charset=utf-8');

    if ($array !== null){
        return json_encode($array, JSON_UNESCAPED_UNICODE);
    }
    exit();
}

    因為文章的創建、編輯、刪除需要登錄后才能操作,因此在初始化的時候需要同時初始化 user 只有和 article 資源。

    然后后面的所有 http 操作都交由 run 方法來完成。在 run 中根據 http 請求獲取請求方法和請求資源。

/**
 * 初始化請求方法
 * @throws Exception
 */
private function _setupRequestMethod()
{
    $this->_requestMethod = $_SERVER['REQUEST_METHOD'];
    if (!in_array($this->_requestMethod, $this->_allowRequestMethods)){
        throw new Exception('請求方法不被允許', 405);
    }
}

/**
 * 初始化請求資源
 * @throws Exception
 */
private function _setupResource()
{
    $path = $_SERVER['PATH_INFO'];
    $params = explode('/', $path);
    $this->_resourceName = $params[1];
    if (!in_array($this->_resourceName, $this->_allowResources)){
        throw new Exception('請求資源不內允許', 400);
    }
    if (!empty($params[2])){
        $this->_id = $params[2];
    }
}

②請求資源類型判斷

然后判斷請求的資源該交給誰處理:users 資源交給 _handleUser 處理, articles 資源交給 _handleArticle 處理。

/**
 * 請求用戶
 * @return array
 * @throws Exception
 */
private function _handleUser()
{
    if ($this->_requestMethod !== 'POST'){
        throw new Exception('請求方法不被允許', 405);
    }
    $body = $this->_getBodyParams();
    if (empty($body['username'])){
        throw new Exception('用戶名不能為空', 400);
    }
    if (empty($body['password'])){
        throw new Exception('密碼不能為空', 400);
    }
    
    //請求用戶資源只有 POST 過來的注冊用戶一種操作,用 SOAP 中的用戶注冊來完成
    return $this->_user->register($body['username'], $body['password']);
}

/**
 * 請求article
 * @return array|mixed|null
 * @throws Exception
 */
private function _handleArticle()
{
    switch ($this->_requestMethod){
        case 'POST':
            return $this->_handleArticleCreate();
            break;
        case 'PUT':
            return $this->_handleArticleEdit();
            break;
        case 'GET':
            if (empty($this->_id)){
                return $this->_handleArticleList();
            }else{
                return $this->_handleArticleView();
            }
            break;
        case 'DELETE':
            return $this->_handleArticleDelete();
            break;
        default:
            throw new Exception('請求方法不被允許', 405);
    }
}

RESTful API 的接口處理從大方向來說已經完成了,接下來便是內部各個方法的完善。

在創建文章、編輯文章、刪除等操做需要登錄,即在請求這接口時要同時傳遞用戶賬號信息。

③用戶賬號信息驗證

/**
 * 用戶登錄
 * @param $PHP_AUTH_USER
 * @param $PHP_AUTH_PW
 * @return mixed
 * @throws Exception
 */
private function _userLogin($PHP_AUTH_USER, $PHP_AUTH_PW)
{
    try{
        return $this->_user->login($PHP_AUTH_USER, $PHP_AUTH_PW);
    }catch (Exception $e){
        if (in_array($e->getCode(),[
            ErrorCode::USERNAME_CANNOT_EMPTY,
            ErrorCode::PASSWORD_CANNOT_EMPTY,
            ErrorCode::USERNAME_OR_PASSWORD_INVALID
        ])){
            throw new Exception($e->getMessage(),400);
        }
        throw new Exception($e->getMessage(), 500);
    }
}
/**
 * 獲取請求參數
 * @return mixed
 * @throws Exception
 */
private function _getBodyParams()
{
    $raw = file_get_contents('php://input');
    if (empty($raw)){
        throw new Exception('請求參數錯誤', 400);
    }
    return json_decode($raw, true);
}

④article 相關請求處理

用戶登錄驗證通過后才能進行后續的 article 相關操作:

/**
 * 創建文章
 * @return array
 * @throws Exception
 */
private function _handleArticleCreate()
{
    $body = $this->_getBodyParams();
    if (empty($body['title'])){
        throw new Exception('文章標題不能為空', 400);
    }
    if (empty($body['content'])){
        throw new Exception('文章內容不能為空', 400);
    }
    //用戶登錄
    $user = $this->_userLogin($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
    try{
        $article = $this->_article->create($body['title'], $body['content'], $user['user_id']);
        return $article;
    }catch (Exception $e){
        if (in_array($e->getCode(), [
            ErrorCode::ARTICLE_TITLE_CANNOT_EMPTY,
            ErrorCode::ARTICLE_CONTENT_CANNOT_EMPTY
        ])){
            throw new Exception($e->getMessage(), 400);
        }
        throw new Exception($e->getMessage(), 500);
    }
}

/**
 * 編輯文章
 * @return array|mixed
 * @throws Exception
 */
private function _handleArticleEdit()
{
    $user = $this->_userLogin($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
    try{
        $article = $this->_article->view($this->_id);
        if ($article['user_id'] !== $user['user_id']){
            throw new Exception('您無權編輯此文章', 403);
        }
        $body = $this->_getBodyParams();
        $title = empty($body['title']) ? $article['title'] : $body['title'];
        $content = empty($body['content']) ? $article : $body['content'];
        if ($title === $article['title'] && $content === $article['content']){
            return $article;
        }
        return $this->_article->edit($article['article_id'], $title, $content, $user['user_id']);
    }catch (Exception $e){
        if ($e->getCode()<100){
            if ($e->getCode() == ErrorCode::ARTICLE_NOT_FOUND){
                throw new Exception($e->getMessage(), 404);
            }else{
                throw new Exception($e->getMessage(), 400);
            }
        }else{
            throw $e;
        }
    }
}

/**
 * 文章刪除
 * @return null
 * @throws Exception
 */
private function _handleArticleDelete()
{
    $user = $this->_userLogin($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
    try{
        $article = $this->_article->view($this->_id);
        if ($article['user_id'] !== $user['user_id']){
            throw new Exception('您無權編輯此文章', 403);
        }
        $this->_article->delete($article['article_id'], $user['user_id']);
        return null;
    }catch (Exception $e){
        if ($e->getCode()<100){
            if ($e->getCode() == ErrorCode::ARTICLE_NOT_FOUND){
                throw new Exception($e->getMessage(), 404);
            }else{
                throw new Exception($e->getMessage(), 400);
            }
        }else{
            throw $e;
        }
    }
}

/**
 * 獲取文章列表
 * @return mixed
 * @throws Exception
 */
private function _handleArticleList()
{
    $user = $this->_userLogin($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
    $page = isset($_GET['page']) ? $_GET['page'] : 1;
    $size = isset($_GET['size']) ? $_GET['size'] : 10;

    if ($size > 100){
        throw new Exception('分頁大小最大為100', 400);
    }
    return $this->_article->getList($user['user_id'], $page, $size);
}

/**
 * 獲取單篇文章
 * @return mixed
 * @throws Exception
 */
private function _handleArticleView()
{
    try{
        return $this->_article->view($this->_id);
    }catch (Exception $e){
        if ($e->getCode() == ErrorCode::ARTICLE_NOT_FOUND){
            throw new Exception($e->getMessage(), 404);
        }else{
            throw new Exception($e->getMessage(), 400);
        }
    }
}

⑤API接口調試

接口調試是必不可少的,調試可以檢查代碼是否有錯誤,檢查接口返回的數據是否與我們預想中的一樣。接口調試時要驗證,插件的狀態碼以及我們程序返回的狀態和提示信息是否一致,有時雖然不報錯,但如果插件的狀態碼提示和我們返回的提示信息不一致也是有問題的。

DHC client 插件

Postman 插件

兩種都是chrome 瀏覽器插件,隨便用哪一種

這里用的是PostMan插件

API接口調試


文章版權及轉載聲明:

作者:心月 本文地址:http://www.eojird.tw/other/254.html發布于 2019-07-01
文章轉載或復制請以超鏈接形式并注明出處心月IT博客

分享到:
贊(

發表評論

快捷輸入:

    評論列表 (有 0 條評論,人圍觀)參與討論