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插件