# RestfulApi **Repository Path**: paultest/RestfulApi ## Basic Information - **Project Name**: RestfulApi - **Description**: 简单介绍RestfulApi的用法 - **Primary Language**: PHP - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2018-02-02 - **Last Updated**: 2021-07-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RestfulApi 简单介绍RestfulApi的用法,主要是参考慕课网的《Restful API实战》教程 ## 1-1 restful简介及资源介绍 #### 1. Restful的介绍 本质:基于资源的一种软件架构风格 核心:面向资源 解决的问题: * 降低开发的复杂性 * 提高系统的可伸缩性 #### 2. 设计概念和准则 网络上的所有事物都可以被抽象为资源 每一个资源都有唯一的资源标识,对资源的操作不会改变这些标识 所有的操作都是无状态的 #### 3. 资源 所谓的资源,就是网络上的一个实体,或者说是网络上的一个具体信息 ## 2-1 Restful中HTTP协议介绍 #### 1. HTTP的协议 HTTP全称是HyperText Transfer Protocal,即:超文本传输协议,从1990年开始就在WWW上广泛应用,是现今在WWW上应用最多的协议。 Http是应用层协议,特点是简洁快速。 当你上网浏览网页的时候,浏览器和Web服务器之间就会通过HTTP在Internet上进行数据的发送和接收。 Http是一个基于请求/响应模式的、无状态的协议。即我们通常所说的Request/Response 比如:schema://host[:port]/path[?query-string][#anchor] * scheme:指定底层使用的协议(如http、https、ftp等) * host:服务器的IP地址或者域名 * port:服务器端口,默认为80 * path:访问资源的路径 * query-string:发送给http服务器的数据 * anthor:锚 #### 2. HTTP协议-请求 组成格式:请求行、消息报头、请求正文 #### 3. 请求行 请求行的格式如下: Method Request-URL HTTP-Version CRLF * Method:请求方法(get、post等) * Request-URL:一个统一资源标识符,即请求地址 * HTTP-Version:请求的HTTP协议版本 * CRLF:回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符) 比如:GET / HTTP/1.1 CRLF #### 4. 请求方法 * GET:请求获取Request-URL所标识的资源 * POST:在Request-URL所标识的资源后附加新的数据 * HEAD:请求获取由Request-URL所标识的资源的响应消息报头 * PUT:请求服务器存储一个资源,并用Request-URL作为其标识 * DELETE:请求服务器删除Request-URL所标识的资源 * TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断 * OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求 #### 5. HTTP协议-响应 组成格式:状态行、消息报头、响应正文 #### 6. 状态行 状态行的格式如下: HTTP-Version Status-Code Reason-Phrase CRLF * HTTP-Version:服务器HTTP协议的版本 * Status-Code:服务器发回的响应状态代码 * Reason-Phrase:状态代码的文本描述 比如:HTTP/1.1 200 OK #### 7. 常用状态码 * 200 OK :客户端请求成功 * 400 Bad Request :客户端请求有语法错误,不能被服务器所理解 * 401 Unauthorized :服务器收到请求,但是拒绝提供服务 * 404 Not Found :请求资源不存在 * 500 Internal Server Error :服务器发生不可预期的错误 * 503 Server Unavailable :服务器当前不能处理客户端的请求 ## 2-2 架构区别 #### 1. SOAP WebService架构 WebService 是一种跨编程语言和跨操作系统平台的远程调用技术。 WebService通过HTTP协议发送请求和接收结果时采用XML格式封装,并增加了一些特定的HTTP消息头,这些特定的HTTP消息头和XML内容格式就是SOAP协议 #### 2. Restful架构和SOAP架构的区别 * 效率和易用性 SOAP由于各种需求不断扩充其本身协议的内容,导致在SOAP处理方面的性能有所下降,同时在易用性方面以及学习成本上也有所增加,Restful由于其面向资源接口设计以及操作抽象简化了开发者的不良设计,同时也最大限度的利用了HTTP最初的应用协议设计理念 * 安全性 Restful对于资源型服务接口来说很合适,同时特别适合对于效率要求很高,但是对于安全要求不高的场景。而SOAP的成熟性可以给需要提供给多开发语言,对于安全性要求较高的接口设计带来便利。 ## 3-1 Restful设计要素 #### 1. Restful的设计要素 * 资源路径(URL) * HTTP动词(请求方式) * 过滤信息 * 状态码 * 错误处理 * 返回结果 #### 2. 资源路径 在Restful架构中,每个网址代表一种资源,所以网址中不能有动词,只能有名词。一般来说API中的名词应该使用复数。 比如:有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样: https://api.example.com/v1/zoos https://api.example.com/v1/animals #### 3. HTTP动词 对于资源的操作(CURD),由HTTP动词表示 * GET:从服务器取出资源(一项或多项) * POST:在服务器新建一个资源 * PUT:在服务器更新资源(客户端提供改变后的完整资源) * PATCH:在服务器更新资源(客户端提供改变的属性) * DELETE:从服务器删除资源 比如: * POST /zoos:新建一个动物园 * GET /zoos/ID:获取某个指定动物园的信息 * PUT /zoos/ID:更新某个指定动物园的信息 * DELETE /zoos/ID:删除某个动物园 #### 4. 过滤信息 如果记录数量很多,服务器不可能都将它们返回给用户,API应该提供参数,过滤返回结果 比如: * ?offset=10:指定返回记录的开始位置 * ?page=2&per_page=100:指定第几页,以及每页的记录数 * ?sortby=name&order=asc:指定返回结果排序,以及排序顺序 * ?animal_type_id=1:指定筛选条件 #### 5. 状态码 服务器向用户返回的状态码和提示信息,使用标准HTTP状态码 比如: * 200 OK * 201 CREATED(新建或修改成功) * 204 NO CONTENT(删除数据成功) * 400 BAD REQUEST(请求有错误) * 401 Unauthorized(用户没有验证) * 403 Forbidden(用户访问被禁止) * 422 Unprocesable Entity(创建对象时,验证错误) * 500 Internal Server Error(服务器内部错误) #### 6. 错误处理 如果状态码是4XX或者是5XX,就应该向用户返回出错信息。 一把来说,返回的信息中将error作为键名,出错信息作为键值即可: ``` { "error" : "参数错误" } ``` #### 7. 返回结果 针对不同操作,服务器向用户返回的结果应该符合以下规范: * GET/ collections:返回资源对象列表(数组) * GET/ collection/identity:返回单个资源对象 * POS/ collections:返回新生成的资源对象 * PUT/collections/identity:返回完整的资源对象 * PATCH/collections/identity:返回被修改的属性 * DELETE/collections/identity:返回一个空文档 ## 3-2 DHC Client 我这里是使用postman来代替 ## 3-3 本地开发环境搭建 #### 1. 开发环境搭建 * 下载UPUPW.NET集成环境 * 添加虚拟主机以及取消跨站目录限制 * 添加虚拟主机的本地hosts解析 ## 3-4 确认设计要素 #### 1. 项目需求 * 用户登录、注册 * 文章发表、编辑、管理、列表 #### 2. 确认设计要素 * 资源路径: /users、/articles * HTTP动词:GET、POST、DELETE、PUT * 过滤信息:文章的分页筛选 * 状态码:200、404、422、403 * 错误处理:输出JSON格式错误信息 * 返回结果:输出JSON数组或JSON对象 ## 3-5 数据库设计 #### 1. 数据库设计 * 用户表:ID、用户名、密码、注册时间 * 文章表:文章ID、标题、内容、发表时间、用户ID 两个表的创建sql为: ``` CREATE TABLE `articles` ( `article_id` int(30) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(40) NOT NULL, `content` text NOT NULL, `user_id` int(30) unsigned NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`article_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `users` ( `user_id` int(32) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(20) NOT NULL, `password` char(32) NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`user_id`), KEY `created_at` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ## 3-6 用户业务逻辑 #### 1. 目录结构 目录结构如下: ![Alt1](./1.png) #### 2. 具体代码 具体的代码如下 入口文件index.php: ``` register('admin2', '123456')); // 登录 var_dump($user->login('admin1', '123456')); ``` 数据库配置文件lib/db.php: ``` exec("SET NAMES 'UTF8'"); // 默认pdo查询出来的所有字段的类型都是string类型的,如果想要实现数据库的类型是怎样,pdo查出来的类型就是怎样的话那么可以做下面的设置 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); return $pdo; ``` 错误定义码文件lib/ErrorCode.php: ``` _db = $db; } /** * 用户登录 * @param $username * @param $password * @return array * @throws Exception */ public function login($username, $password) { if (empty($username)) { throw new Exception('用户名不能为空', ErrorCode::USERNAME_CANNOT_EMPTY); } if (empty($password)) { throw new Exception('密码不能为空', ErrorCode::PASSWORD_CANNOT_EMPTY); } $sql = 'SELECT `user_id`, `username`, `created_at` FROM `users` WHERE `username` = :username AND `password` = :password'; $password = $this->_md5($password); $stmt = $this->_db->prepare($sql); $stmt->bindParam(':username', $username, PDO::PARAM_STR); $stmt->bindParam(':password', $password, PDO::PARAM_STR); if (!$stmt->execute()) { throw new Exception('服务器内部错误', ErrorCode::SERVER_INTERNAL_ERROR); } $user = $stmt->fetch(PDO::FETCH_ASSOC); if (empty($user)) { throw new Exception('用户名或密码错误', ErrorCode::USERNAME_OR_PASSWORD_INVALID); } return $user; } /** * 用户注册 * @param $username * @param $password * @return array * @throws Exception */ public function register($username, $password) { if (empty($username)) { throw new Exception('用户名不能为空', ErrorCode::USERNAME_CANNOT_EMPTY); } if (empty($password)) { throw new Exception('密码不能为空', ErrorCode::PASSWORD_CANNOT_EMPTY); } if ($this->_isUsernameExists($username)) { throw new Exception('用户名已存在', ErrorCode::USERNAME_EXISTS); } // 写入数据库 $sql = 'INSERT INTO `users`(`username`, `password`, `created_at`) VALUES (:username, :password, :created_at)'; $stmt = $this->_db->prepare($sql); $password = $this->_md5($password); $created_at = date('Y-m-d H:i:s', time()); $stmt->bindParam(':username', $username, PDO::PARAM_STR); $stmt->bindParam(':password', $password, PDO::PARAM_STR); $stmt->bindParam(':created_at', $created_at); if (!$stmt->execute()) { throw new Exception('注册失败', ErrorCode::REGISTER_FAILED); } return [ 'user_id' => $this->_db->lastInsertId(), 'username' => $username, 'password' => $password ]; } /** * md5加密 * @param $string * @param $key * @return string */ private function _md5($string, $key = 'password') { return md5($string . $key); } /** * 判断用户名是否存在 * @param $username * @return bool */ private function _isUsernameExists($username) { $exists = false; $sql = 'SELECT * FROM `users` WHERE `username` = :username'; $stmt = $this->_db->prepare($sql); $stmt->bindParam(':username', $username, PDO::PARAM_STR); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (!empty($result)) { $exists = true; } return $exists; } } ``` 测试的话,直接运行index.php即可实现注册和登录 ## 3-7 文章业务逻辑 #### 1. 具体代码 入口文件index.php: ``` register('admin2', '123456')); // 登录 // var_dump($user->login('admin1', '123456')); // 发表文章 // var_dump($article->create('这个是标题', '这个是内容', 6)); // 查看一篇文章详情 // var_dump($article->view(2)); // 修改一篇文章 // var_dump($article->edit(2, '新的标题2', '新的内容2', 6)); // 删除一篇文章 // var_dump($article->delete(6, 6)); // 获取文章列表 var_dump($article->getList(6, 2, 3)); ``` 错误定义码文件lib/ErrorCode.php: ``` _db = $db; } /** * 创建文章 * @param $title * @param $content * @param $user_id * @return array * @throws Exception */ public function create($title, $content, $user_id) { if (empty($title)) { throw new Exception('文章标题不能为空', ErrorCode::ARTICLE_TITLE_CANNOT_EMPTY); } if (empty($content)) { throw new Exception('文章内容不能为空', ErrorCode::ARTICLE_CONTENT_CANNOT_EMPTY); } // 写入数据库 $sql = 'INSERT INTO `articles`(`title`, `content`, `user_id`, `created_at`) VALUES(:title, :content, :user_id, :created_at)'; $created_at = date('Y-m-d H:i:s', time()); $stmt = $this->_db->prepare($sql); $stmt->bindParam(':title', $title, PDO::PARAM_STR); $stmt->bindParam(':content', $content, PDO::PARAM_STR); $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT); $stmt->bindParam(':created_at', $created_at); if (!$stmt->execute()) { throw new Exception('发表文章失败', ErrorCode::ARTICLE_CREATE_FAILED); } return [ 'article_id' => $this->_db->lastInsertId(), 'title' => $title, 'content' => $content ]; } /** * 获取文章详情 * @param $article_id * @return mixed * @throws Exception */ public function view($article_id) { if (empty($article_id)) { throw new Exception('文章ID不能为空', ErrorCode::ARTICLE_ID_CANNOT_EMPTY); } $sql = 'SELECT * FROM `articles` WHERE `article_id` = :article_id'; $stmt = $this->_db->prepare($sql); $stmt->bindParam(':article_id', $article_id, PDO::PARAM_INT); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); if (empty($result)) { throw new Exception('文章不存在', ErrorCode::ARTICLE_NOT_FOUND); } return $result; } /** * 修改文章 * @param $article_id * @param $title * @param $content * @param $user_id * @return array|mixed * @throws Exception */ public function edit($article_id, $title, $content, $user_id) { // 获取文章详情 $article = $this->view($article_id); if ($article['user_id'] !== $user_id) { throw new Exception('您无权编辑该文章', ErrorCode::PERMISSION_DENIED); } $title = empty($title) ? $article['title'] : $title; $content = empty($content) ? $article['content'] : $content; if ($title === $article['title'] && $content === $article['content']) { return $article; } $sql = 'UPDATE `articles` SET `title` = :title, `content` = :content WHERE `article_id` = :article_id'; $stmt = $this->_db->prepare($sql); $stmt->bindParam(':title', $title, PDO::PARAM_STR); $stmt->bindParam(':content', $content, PDO::PARAM_STR); $stmt->bindParam(':article_id', $article_id, PDO::PARAM_INT); if (!$stmt->execute()) { throw new Exception('文章编辑失败', ErrorCode::ARTICLE_EDIT_FAILED); } return [ 'article_id' => $article_id, 'title' => $title, 'content' => $content, 'created_at' => $article['created_at'] ]; } /** * 删除文章 * @param $article_id * @param $user_id * @return array|bool * @throws Exception */ public function delete($article_id, $user_id) { // 获取文章详情 $article = $this->view($article_id); if ($article['user_id'] !== $user_id) { throw new Exception('您无权删除该文章', ErrorCode::PERMISSION_DENIED); } $sql = 'DELETE FROM `articles` WHERE `article_id` = :article_id AND `user_id` = :user_id'; $stmt = $this->_db->prepare($sql); $stmt->bindParam(':article_id', $article_id, PDO::PARAM_INT); $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT); if (!$stmt->execute()) { throw new Exception('文章删除失败', ErrorCode::ARTICLE_DELETE_FAILED); } return true; } /** * 读取文章列表 * @param $user_id * @param int $page 第几页 默认第一页 * @param int $size 每一页的数量 默认10 * @return array * @throws Exception */ public function getList($user_id, $page = 1, $size = 10) { if ($size > 100) { throw new Exception('分页大小最大为100', ErrorCode::PAGE_SIZE_TOO_BIG); } $sql = 'SELECT * FROM `articles` WHERE `user_id` = :user_id LIMIT :limit, :offset'; $limit = ($page - 1) * $size; $limit = $limit < 0 ? 0 : $limit; $stmt = $this->_db->prepare($sql); $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT); $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); $stmt->bindParam(':offset', $size, PDO::PARAM_INT); $stmt->execute(); $result = $stmt->fetchAll(PDO::FETCH_ASSOC); return $result; } } ``` 测试的话,直接运行index.php即可实现注册和登录 ## 3-8 RestfulApi设计要素 #### 1. 设计要素 * 资源路径:如:users,articles * HTTP动词:如:get,post,put,delete * 过滤信息:如:limit分页处理 * 状态码:如:200,403 * 错误处理:如:抛出异常,返回提示信息 * 返回结果:如:返回的格式和状态码 #### 2. 状态码 状态码的设计如下: ``` /** 用户注册 */ 请求方法不被允许 405 用户名不能为空 USERNAME_CANNOT_EMPTY 400 密码不能为空 PASSWORD_CANNOT_EMPTY 400 用户名已存在 USERNAME_EXIST 400 注册失败 REGISTER_FAILED 500 /** 创建文章 */ 用户名不能为空 USERNAME_CANNOT_EMPTY 401 密码不能为空 PASSWORD_CANNOT_EMPTY 401 用户名或密码错误 USERNAME_OR_PASSWORD_INVALID 401 服务器内部错误 SERVER_INTERNAL_ERROR 500 文章标题不能为空 ARTICLE_TITLE_CANNOT_EMPTY 400 文章内容不能为空 ARTICLE_CONTENT_CANNOT_EMPTY 400 发表文章失败 ARTICLE_CREATE_FAILED 500 /** 修改文章 */ 用户名不能为空 USERNAME_CANNOT_EMPTY 401 密码不能为空 PASSWORD_CANNOT_EMPTY 401 用户名或密码错误 USERNAME_OR_PASSWORD_INVALID 401 服务器内部错误 SERVER_INTERNAL_ERROR 500 文章ID不能为空 ARTICLE_ID_CANNOT_EMPTY 400 您无权编辑该文章 PERMISSION_DENIED 403 文章不存在 ARTICLE_NOT_FOUND 404 文章编辑失败 ARTICLE_EDIT_FAILED 500 /** 删除文章 */ 用户名不能为空 USERNAME_CANNOT_EMPTY 401 密码不能为空 PASSWORD_CANNOT_EMPTY 401 用户名或密码错误 USERNAME_OR_PASSWORD_INVALID 401 服务器内部错误 SERVER_INTERNAL_ERROR 500 文章ID不能为空 ARTICLE_ID_CANNOT_EMPTY 400 文章不存在 ARTICLE_NOT_FOUND 404 您无权删除该文章 PERMISSION_DENIED 403 文章删除失败 ARTICLE_DELETE_FAILED 500 /** 获取文章列表 */ 用户名不能为空 USERNAME_CANNOT_EMPTY 401 密码不能为空 PASSWORD_CANNOT_EMPTY 401 用户名或密码错误 USERNAME_OR_PASSWORD_INVALID 401 服务器内部错误 SERVER_INTERNAL_ERROR 500 分页大小最大为100 PAGE_SIZE_TOO_BIG 400 /** 获取文章详情 */ 文章ID不能为空 ARTICLE_ID_CANNOT_EMPTY 400 文章不存在 ARTICLE_NOT_FOUND 404 ``` 具体的状态码可以参考: [【HTTP状态码】HTTP状态码详解【转载】](http://20f0d0fd.wiz03.com/share/s/0wYd3Z0a-kXV2Aimaz0qG6X51_iPEo1CYkw22tWqMJ0fRyt0) ## 3-9 初始化运行参数 #### 1. 设计结构 由于restful是根据资源和请求方法来执行具体的方法的,比如根据域名后/articles或者是/users来定位资源,根据请求方法来定位具体的方法,所以设计的时候需要定义一个新的入口,可以新建个目录比如命名为restful,目录下新建个index.php,然后请求域名为 域名/restful/index.php/资源/其他参数 #### 2. 设计模式 采用适配器模式,在lib下新建个Restful.php类,根据资源和请求方法来对Article类和User类进行再封装 #### 3. 目录结构 目录结构如下: ![Alt2](./2.png) #### 2. 入口文件 入口文件代码为restful/index.php: ``` run(); ``` #### 3. 适配器文件 适配器文件代码为 lib/Restful.php: ``` 'OK', 204 => 'No Content', 400 => 'Bad request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 500 => 'Server Internal Error' ]; 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') { $this->_json($this->_handleUser()); } elseif ($this->_resourceName == 'articles') { $this->_json($this->_handleArticle()); } } catch (Exception $e) { $this->_json(['error' => $e->getMessage(), 'code' => $e->getCode()]); } } /** * 初始化请求方法 */ private function _setupRequestMethod() { $this->_requestMethod = $_SERVER['REQUEST_METHOD']; if (!in_array($this->_requestMethod, $this->_allowRequestMethods)) { throw new Exception('请求方法不被允许', 405); } } /** * 初始化请求资源(用户资源还是文章资源) */ private function _setupResource() { $path = $_SERVER['QUERY_STRING']; $params = explode('type=', $path); $resource = $params[1]; if (strstr($resource, '&')) { $resource = explode('&', $params[1])[0]; } if (!in_array($resource, $this->_allowResources)) { throw new Exception('请求资源不被允许', 400); } $this->_resourceName = $resource; // 初始化资源标识符(id) if (strstr($path, 'id=')) { $params = explode('id=', $path); $resource = $params[1]; if (strstr($resource, '&')) { $resource = explode('&', $params[1])[0]; } $this->_id = $resource; } } /** * 输出json * @param $array * @internal param $array */ private function _json($array) { if ($array === null) { $array['code'] = 204; } if (isset($array['code']) && $array['code'] > 0 && $array['code'] != 200 && $array['code'] != 204) { header('HTTP/1.1 ' . $array['code'] . ' ' . $this->_statusCode[$array['code']]); } header('Content-Type:application/json;charset=utf-8'); echo json_encode($array, JSON_UNESCAPED_UNICODE); exit(); } /** * 请求用户资源(注册) */ private function _handleUser(){} /** * 请求文章资源(增删改查) */ private function _handleArticle() { switch ($this->_requestMethod) { case 'POST': // 创建文章 return $this->_handleArticleCreate(); case 'PUT': // 修改文章 return $this->_handleArticleEdit(); case 'DELETE': // 删除文章 return $this->_handleArticleDelete(); case 'GET': // 获取文章列表 if (empty($this->_id)) { return $this->_handleArticleList(); } // 获取文章详情 return $this->_handleArticleView(); default: throw new Exception('请求方法不被允许', 405); } } /** * 获取请求参数 * @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); } /** * 创建文章(需要用户授权登录) */ private function _handleArticleCreate(){} /** * 编辑文章(需要用户授权登录) */ private function _handleArticleEdit() {} /** * 删除文章(需要用户授权登录) */ private function _handleArticleDelete(){} /** * 读取文章列表(需要用户授权登录) */ private function _handleArticleList(){} /** * 查看一篇文章详情(不需要用户授权登录) */ private function _handleArticleView(){} } ``` ## 3-10 完善用户API & 3-11 完善文章API #### 1. 代码 lib/Restful.php ``` 'OK', 204 => 'No Content', 400 => 'Bad request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 500 => 'Server Internal Error' ]; 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') { $this->_json($this->_handleUser()); } elseif ($this->_resourceName == 'articles') { $this->_json($this->_handleArticle()); } } catch (Exception $e) { $this->_json(['error' => $e->getMessage(), 'code' => $e->getCode()]); } } /** * 初始化请求方法 */ private function _setupRequestMethod() { $this->_requestMethod = $_SERVER['REQUEST_METHOD']; if (!in_array($this->_requestMethod, $this->_allowRequestMethods)) { throw new Exception('请求方法不被允许', 405); } } /** * 初始化请求资源(用户资源还是文章资源) */ private function _setupResource() { $path = $_SERVER['QUERY_STRING']; $params = explode('type=', $path); $resource = $params[1]; if (strstr($resource, '&')) { $resource = explode('&', $params[1])[0]; } if (!in_array($resource, $this->_allowResources)) { throw new Exception('请求资源不被允许', 400); } $this->_resourceName = $resource; // 初始化资源标识符(id) if (strstr($path, 'id=')) { $params = explode('id=', $path); $resource = $params[1]; if (strstr($resource, '&')) { $resource = explode('&', $params[1])[0]; } $this->_id = $resource; } } /** * 输出json * @param $array * @internal param $array */ private function _json($array) { if ($array === null) { $array['code'] = 204; } if (isset($array['code']) && $array['code'] > 0 && $array['code'] != 200 && $array['code'] != 204) { header('HTTP/1.1 ' . $array['code'] . ' ' . $this->_statusCode[$array['code']]); } header('Content-Type:application/json;charset=utf-8'); echo json_encode($array, JSON_UNESCAPED_UNICODE); exit(); } /** * 请求用户资源(注册) * @return array * @throws Exception */ private function _handleUser() { if ($this->_requestMethod != 'POST') { throw new Exception('请求方法不被允许', 405); } try { // 获取请求参数 $body = $this->_getBodyParams(); // 用户注册 return $this->_user->register($body['username'], $body['password']); } catch (Exception $e) { // 用户操作错误的编码数组 $UserErrorCode = [ ErrorCode::USERNAME_CANNOT_EMPTY, ErrorCode::PASSWORD_CANNOT_EMPTY, ErrorCode::USERNAME_EXISTS ]; // 用户操作错误归为400错误,其他的则为500错误 if (in_array($e->getCode(), $UserErrorCode)) { throw new Exception($e->getMessage(), 400); } throw new Exception($e->getMessage(), 500); } } /** * 请求文章资源(增删改查) * @return array|bool|mixed * @throws Exception */ private function _handleArticle() { switch ($this->_requestMethod) { case 'POST': // 创建文章 return $this->_handleArticleCreate(); case 'PUT': // 修改文章 return $this->_handleArticleEdit(); case 'DELETE': // 删除文章 return $this->_handleArticleDelete(); case 'GET': // 获取文章列表 if (empty($this->_id)) { return $this->_handleArticleList(); } // 获取文章详情 return $this->_handleArticleView(); default: throw new Exception('请求方法不被允许', 405); } } /** * 获取请求参数 * @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); } /** * 创建文章(需要用户授权登录) * @return array * @throws Exception */ private function _handleArticleCreate() { // 用户登录 $user = $this->_userLogin(); try { // 获取请求参数 $body = $this->_getBodyParams(); // 创建文章 $article = $this->_article->create($body['title'], $body['content'], $user['user_id']); return $article; } catch (Exception $e) { // 文章操作错误的编码数组 $ArticleErrorCode = [ ErrorCode::ARTICLE_TITLE_CANNOT_EMPTY, ErrorCode::ARTICLE_CONTENT_CANNOT_EMPTY, ]; // 文章操作归为400错误 if (in_array($e->getCode(), $ArticleErrorCode)) { throw new Exception($e->getMessage(), 400); } // 其他的则为500错误 throw new Exception($e->getMessage(), 500); } } /** * 编辑文章(需要用户授权登录) * @return array * @throws Exception */ private function _handleArticleEdit() { // 用户登录 $user = $this->_userLogin(); try { // 获取请求参数 $body = $this->_getBodyParams(); return $this->_article->edit($this->_id, $body['title'], $body['content'], $user['user_id']); } catch (Exception $e) { switch ($e->getCode()) { // 文章ID不能为空 case ErrorCode::ARTICLE_ID_CANNOT_EMPTY: throw new Exception($e->getMessage(), 400); // 文章不存在 case ErrorCode::ARTICLE_NOT_FOUND: throw new Exception($e->getMessage(), 404); // 当前用户无权限编辑该文章 case ErrorCode::PERMISSION_DENIED: throw new Exception($e->getMessage(), 403); // 其他错误 default: throw new Exception($e->getMessage(), 500); } } } /** * 删除文章(需要用户授权登录) * @return bool * @throws Exception */ private function _handleArticleDelete() { // 用户登录 $user = $this->_userLogin(); try { return $this->_article->delete($this->_id, $user['user_id']); } catch (Exception $e) { switch ($e->getCode()) { // 文章ID不能为空 case ErrorCode::ARTICLE_ID_CANNOT_EMPTY: throw new Exception($e->getMessage(), 400); // 文章不存在 case ErrorCode::ARTICLE_NOT_FOUND: throw new Exception($e->getMessage(), 404); // 当前用户无权限删除该文章 case ErrorCode::PERMISSION_DENIED: throw new Exception($e->getMessage(), 403); // 其他错误 default: throw new Exception($e->getMessage(), 500); } } } /** * 读取文章列表(需要用户授权登录) * @return array * @throws Exception */ private function _handleArticleList() { // 用户登录 $user = $this->_userLogin(); try { // 第几页 $page = isset($_GET['page']) ? $_GET['page'] : 1; // 每一页的数量 $size = isset($_GET['size']) ? $_GET['size'] : 10; return $this->_article->getList($user['user_id'], $page, $size); } catch (Exception $e) { if ($e->getCode() == ErrorCode::PAGE_SIZE_TOO_BIG) { throw new Exception($e->getMessage(), 400); } throw new Exception($e->getMessage(), 500); } } /** * 查看一篇文章详情(不需要用户授权登录) * @return mixed * @throws Exception */ private function _handleArticleView() { try { return $this->_article->view($this->_id); } catch (Exception $e) { switch ($e->getCode()) { // 文章ID不能为空 case ErrorCode::ARTICLE_ID_CANNOT_EMPTY: throw new Exception($e->getMessage(), 400); // 文章不存在 case ErrorCode::ARTICLE_NOT_FOUND: throw new Exception($e->getMessage(), 404); // 其他错误 default: throw new Exception($e->getMessage(), 500); } } } /** * 用户授权登录(基于HTTP请求头的身份认证) * @return array * @throws Exception */ private function _userLogin() { try { // 用户名 $PHP_AUTH_USER = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : ''; // 用户密码 $PHP_AUTH_PW = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; return $this->_user->login($PHP_AUTH_USER, $PHP_AUTH_PW); } catch (Exception $e) { // 用户登录操作错误的编码数组 $UserLoginErrorCode = [ ErrorCode::USERNAME_CANNOT_EMPTY, ErrorCode::PASSWORD_CANNOT_EMPTY, ErrorCode::USERNAME_OR_PASSWORD_INVALID ]; // 用户登录操作错误归为401错误 if (in_array($e->getCode(), $UserLoginErrorCode)) { throw new Exception($e->getMessage(), 401); } // 其他的则为500错误 throw new Exception($e->getMessage(), 500); } } } ``` #### 2. 测试 ##### a. 用户资源 用户资源只开放注册的功能,而用户登录的功能是后面的文章资源需要的,所以这里只测试用户注册即可 域名:http://域名/RestfulApi/restful/index.php?type=users 方法:post Headers参数: ``` Content-Type:application/json ``` Body参数: ``` { "username": "admintest5", "password": "123456" } ``` 如图: ![Alt3](./3.png) 由于数据库中已经有admintest5这个用户,所以返回400,并且返回错误结果 ##### b. 创建文章 由于创建文章,是需要用户授权登录的,所以Headers中是需要加上账号密码的信息的 域名:http://域名/RestfulApi/restful/index.php?type=articles 方法:post Headers参数: ``` Content-Type:application/json Authorization:Basic YWRtaW50ZXN0NToxMjM0NTY= ``` 如图: ![Alt4](./4.png) 其中Authorization:Basic 后面的编码是 admintest4:123456 经过base64编码得到的 Body参数: ``` { "title": "这个是标题", "content": "123123" } ``` 结果为: ``` { "article_id": "20", "title": "这个是标题", "content": "123123" } ``` ##### c. 编辑文章 域名:http://域名/RestfulApi/restful/index.php?type=articles&id=12 方法:put Header参数:同上 Body参数: ``` { "title": "这个是修改标题2", "content": "这个是修改过的内容" } ``` ##### d. 删除文章 域名:http://域名/RestfulApi/restful/index.php?type=articles&id=12 方法:delete Header参数:同上 Body参数:无 ##### e. 获取文章列表 域名:http://域名/RestfulApi/restful/index.php?type=articles 方法:get Header参数:同上 Body参数:无 ##### f. 获取文章详情 域名:http://域名/RestfulApi/restful/index.php?type=articles&id=13 方法:get Header参数: ``` Content-Type:application/json ``` Body参数:无