第3章:对话规则:理解HTTP协议与RESTful API设计
章节介绍
学习目标
通过本章学习,你将能够:
- 深刻理解HTTP协议在Web通信中的核心作用.
- 掌握关键HTTP方法(GET, POST, PUT, PATCH, DELETE)的语义与正确使用场景.
- 解读常见HTTP状态码(如200, 404, 500)的含义,并能在API中正确应用.
- 理解请求头与响应头(如Content-Type, Authorization)的作用并熟练使用.
- 掌握RESTful架构风格的核心思想与设计规范.
- 熟练使用JSON作为API数据交换格式.
在本教程中的作用
本章是连接"PHP基础与数据库操作"(第1、2章)与"动手构建API"(第4章)的关键桥梁.它将为你揭示Web应用程序(包括你的手机App、浏览器中的网页)与服务器之间进行"对话"所遵循的底层规则.不理解HTTP和RESTful API,就无法构建出规范、易用且安全的后端服务.掌握了本章的"对话规则",你才能在第4章中编写出合格的API代码,让前端开发者能够愉快地与你协作.
与前面章节的衔接
- 在第2章,你学会了如何使用PHP操作MySQL数据库,这为你存储和提供数据做好了准备.
- 在本章,你将学习数据如何通过一套标准的规则(HTTP协议)被请求和返回.数据库是"仓库",而HTTP协议是"运输协议和交通规则".
- 掌握了本章知识后,你在第4章的任务就是将PHP代码与这些HTTP规则结合起来,创建出真正的数据接口.
本章主要内容概览
本章将从HTTP协议的基础讲起,通过比喻帮助你理解客户端与服务器如何通信.然后,我们将深入HTTP的各个组成部分:方法、状态码、头部.接着,引出并详细讲解现代API设计的流行风格——RESTful架构.最后,我们将通过一个完整的实战项目——"图书管理系统API"设计,将所有理论知识融会贯通,并用Postman工具进行检验.
核心概念讲解
1. HTTP协议基础
概念:HTTP(超文本传输协议)是用于从网络服务器传输超文本(如网页)到本地浏览器的应用层协议.它是Web数据通信的基石,所有浏览器、App与服务器的交互都基于此.
核心模型:客户端-服务器(Client-Server)
- 客户端(Client):发起请求的一方.例如:你的浏览器(Chrome, Firefox)、手机App、Postman测试工具.
- 服务器(Server):接收请求、处理并返回响应的一方.例如:运行着你编写的PHP代码的Web服务器(Apache/Nginx).
- 交互过程:客户端发送一个HTTP请求(Request)到服务器 -> 服务器处理请求 -> 服务器返回一个HTTP响应(Response)给客户端.这个过程是无状态(Stateless)的,即服务器不会记住上一次的请求,每次请求都是独立的.
HTTP请求与响应结构
一个完整的HTTP交互包含两部分:请求报文和响应报文.
- HTTP请求报文示例:
GET /api/books/1 HTTP/1.1 // 请求行:方法 + URI + 协议版本 Host: example.com // 请求头(Headers) User-Agent: Mozilla/5.0... Accept: application/json // 客户端希望接收JSON格式 (此处空一行,分隔头部和主体) // 请求主体(Body),GET请求通常没有Body- **请求行**:定义了要执行的动作(GET)、目标资源(`/api/books/1`)和使用的HTTP版本. - **请求头**:包含关于请求的元信息,如客户端信息、接受的数据格式等. - **请求体**:可选,通常用于POST、PUT等方法,携带要发送给服务器的数据(如表单数据、JSON).- HTTP响应报文示例:
HTTP/1.1 200 OK // 状态行:协议版本 + 状态码 + 状态消息 Content-Type: application/json // 响应头(Headers) Content-Length: 85 (此处空一行,分隔头部和主体) { "id": 1, "title": "深入理解计算机系统" } // 响应主体(Body),包含服务器返回的实际数据- **状态行**:告知客户端请求的处理结果(成功200, 未找到404等). - **响应头**:包含关于响应的元信息,如服务器类型、返回数据的格式、缓存指令等. - **响应体**:服务器返回给客户端的实际内容,如HTML、JSON、图片等.2. 关键HTTP方法(动词)
HTTP方法定义了客户端希望对目标资源执行的操作.在RESTful API设计中,方法的选择至关重要.
| 方法 | 语义(做什么) | 是否幂等 | 是否安全 | 典型应用场景(以/books资源为例) |
|---|---|---|---|---|
| GET | 获取(Fetch)资源 | 是 | 是 | 获取图书列表GET /api/books, 获取id为1的图书详情GET /api/books/1 |
| POST | 创建(Create)新资源 | 否 | 否 | 创建一本新图书POST /api/books(数据放在请求体) |
| PUT | 完整更新(Update/Replace)资源 | 是 | 否 | 更新id为1的图书(需提供完整字段)PUT /api/books/1 |
| PATCH | 部分更新(Partial Update)资源 | 否 | 否 | 仅更新id为1的图书的价格字段PATCH /api/books/1 |
| DELETE | 删除(Delete)资源 | 是 | 否 | 删除id为1的图书DELETE /api/books/1 |
重要说明:
- 幂等性(Idempotent):无论请求执行一次还是多次,产生的效果(服务器状态)相同.例如,删除一个资源一次和多次,结果都是"该资源不存在".这对网络重试、保证数据一致性非常重要.GET, PUT, DELETE是幂等的;POST和PATCH通常不是.
- 安全性(Safe):指该方法不应改变服务器状态(即只读).GET是安全的,其他方法都不是.
3. 常见HTTP状态码
状态码是一个3位数字代码,用于表示服务器对请求的处理结果.它分为5类:
| 范围 | 类别 | 含义 | 常见例子 |
|---|---|---|---|
| 1xx | 信息性 | 请求已接收,继续处理 | 100(继续), 101(切换协议) |
| 2xx | 成功 | 请求已成功处理 | 200(OK),201(Created, 资源创建成功), 204(No Content, 成功但无返回体) |
| 3xx | 重定向 | 需要进一步操作以完成请求 | 301(永久重定向), 302(临时重定向) |
| 4xx | 客户端错误 | 请求包含语法错误或无法完成 | 400(Bad Request, 请求参数错误),401(Unauthorized, 需要认证),403(Forbidden, 无权限),404(Not Found, 资源不存在) |
| 5xx | 服务器错误 | 服务器处理请求时出错 | 500(Internal Server Error, 通用服务器错误), 502(网关错误), 503(服务不可用) |
在API开发中的黄金法则:永远返回恰当的状态码.不要所有请求都返回200,然后在响应体里用{"code": 404}表示错误.正确的状态码能让客户端(前端、其他服务)快速、标准地判断请求结果.
4. 请求头与响应头
头部(Headers)是键值对,携带了请求或响应的元数据.
关键请求头:
Content-Type:至关重要.告诉服务器请求体(Body)的格式.例如:application/json,application/x-www-form-urlencoded(表单默认),multipart/form-data(文件上传).Accept: 客户端希望接收的响应体格式.例如:application/json.Authorization: 用于携带认证凭证,如Bearer Token:Authorization: Bearer your_jwt_token_here.User-Agent: 客户端(浏览器/工具)标识.
关键响应头:
Content-Type:至关重要.告诉客户端响应体的实际格式.API必须设置为application/json; charset=utf-8.Cache-Control: 控制缓存行为,如no-cache.Access-Control-Allow-Origin: 用于解决跨域问题(CORS),在第6章详解.
5. RESTful架构风格
REST(Representational State Transfer, 表述性状态转移)是一种软件架构风格,用于设计网络应用程序的API.它强调:
- 以资源为中心:将服务器提供的数据/服务抽象为"资源"(Resource),如用户、文章、订单.每个资源有唯一的标识符(URI).
- 统一的接口:使用标准的HTTP方法(GET/POST/PUT/DELETE等)来操作资源.
- 无状态性:每次请求都包含处理该请求所需的所有信息,服务器不保存会话状态.这提高了可扩展性和可靠性.
- 可缓存:响应应被明确标记为可缓存或不可缓存,以提高性能.
- 分层系统:客户端通常不知道是否直接连接到最终服务器,还是中间的代理(如负载均衡器、缓存服务器).
RESTful API URI设计规范:
- 使用名词(复数形式)表示资源集合,而不是动词.
- 好:
/api/books,/api/users - 差:
/api/getAllBooks,/api/createUser
- 好:
- 将资源的唯一标识(通常是数据库ID)放在URI路径中.
- 好:
/api/books/1(操作ID为1的图书)
- 好:
- 保持URI层级清晰.对于子资源(如某本书的所有评论):
/api/books/1/comments(获取书1的评论)/api/books/1/comments/5(获取/操作书1下ID为5的评论)
- 使用连字符
-提高可读性,避免下划线_. - 版本控制:将API版本号放在URI或头部中.常见方式:
/api/v1/books.
6. 数据交换格式:JSON
JSON(JavaScript Object Notation)已成为现代API数据交换的事实标准,因为它轻量、易读、易解析.
JSON基本结构:
{"key1":"value1","key2":123,"key3":true,"key4":null,"key5":["item1","item2"],"key6":{"nestedKey":"nestedValue"}}- 数据以键值对形式组织.
- 值可以是字符串(双引号)、数字、布尔值、null、数组(方括号
[])或另一个对象(花括号{}). - PHP与JSON互转:
json_encode($php_array_or_object): 将PHP变量转换为JSON字符串.json_decode($json_string, true): 将JSON字符串转换为PHP关联数组(第二个参数为true)或对象.
代码示例
示例1:用PHP模拟简单的HTTP请求与响应
此示例演示服务器端如何获取请求信息并构造响应.
<?php// 示例:simple_request_response.php// 此脚本模拟一个简单的端点,根据不同的查询参数返回不同的响应// 1. 获取客户端请求的方法$requestMethod=$_SERVER['REQUEST_METHOD'];echo"[服务器日志] 接收到{$requestMethod}请求\n";// 2. 获取请求的URI$requestUri=$_SERVER['REQUEST_URI'];echo"[服务器日志] 请求URI:{$requestUri}\n";// 3. 获取查询字符串参数(GET参数)// 例如:访问 /simple_request_response.php?name=John&age=25$name=$_GET['name']??'未知访客';// 使用null合并运算符提供默认值$age=$_GET['age']??0;// 4. 设置响应头 - 告诉浏览器返回的是纯文本,编码为UTF-8header('Content-Type: text/plain; charset=utf-8');// 5. 根据请求方法做出不同响应switch($requestMethod){case'GET':// 返回一个简单的问候信息$responseBody="你好,{$name}!";if($age>0){$responseBody.=" 听说你{$age}岁了.";}// 设置HTTP状态码为200 OKhttp_response_code(200);break;case'POST':// 对于POST请求,我们尝试从php://input流中获取原始数据$rawPostData=file_get_contents('php:// input');$responseBody="你发送了一个POST请求.";if(!empty($rawPostData)){$responseBody.=" 原始数据是: ".$rawPostData;}http_response_code(201);// 201 Created, 常用于POST成功break;default:$responseBody="本示例仅处理GET和POST请求.";http_response_code(405);// 405 Method Not Allowed// 在响应头中告知客户端允许哪些方法(这是405状态码的最佳实践)header('Allow: GET, POST');break;}// 6. 输出响应体echo$responseBody;echo"\n--- 请求处理完毕 ---\n";?>访问http:// 你的域名/simple_request_response.php?name=Alice&age=30,预期输出:
[服务器日志] 接收到 GET 请求 [服务器日志] 请求URI: /simple_request_response.php?name=Alice&age=30 你好,Alice! 听说你30岁了. --- 请求处理完毕 ---示例2:设置HTTP状态码和JSON响应
这是构建API接口的核心模式.
<?php// 示例:api_response_demo.php// 演示如何构建一个标准的JSON API响应/** * 发送一个标准的JSON API响应 * * @param mixed $data 要返回的主要数据 * @param int $statusCode HTTP状态码,默认200 * @param string|null $message 可选的人类可读消息 * @param array $errors 可选的错误详情数组 */functionsendJsonResponse($data,$statusCode=200,$message=null,$errors=[]){// 首先,设置HTTP状态码http_response_code($statusCode);// 然后,设置Content-Type头为JSON,并指定UTF-8编码防止中文乱码header('Content-Type: application/json; charset=utf-8');// 构造响应体数组$response=['success'=>$statusCode>=200&&$statusCode<300,// 2xx状态码视为成功'message'=>$message,'data'=>$data,];// 只有在有错误时才包含errors字段if(!empty($errors)){$response['errors']=$errors;}// 将PHP数组编码为JSON字符串并输出echojson_encode($response,JSON_UNESCAPED_UNICODE);// JSON_UNESCAPED_UNICODE确保中文不被编码为\u形式exit;// 通常响应发送后终止脚本执行}// --- 模拟不同的API场景 ---// 场景1:成功获取数据$books=[['id'=>1,'title'=>'PHP从入门到精通','author'=>'张三'],['id'=>2,'title'=>'深入理解MySQL','author'=>'李四'],];// sendJsonResponse($books, 200, '图书列表获取成功');// 场景2:创建资源成功$newBook=['id'=>3,'title'=>'新书','author'=>'王五'];// sendJsonResponse($newBook, 201, '图书创建成功');// 场景3:客户端请求错误(例如,缺少必要参数)$validationErrors=['title'=>['标题不能为空'],'author'=>['作者名称至少2个字符'],];// sendJsonResponse(null, 400, '请求参数验证失败', $validationErrors);// 场景4:资源未找到// sendJsonResponse(null, 404, '请求的图书不存在');// 场景5:需要认证(模拟)// sendJsonResponse(null, 401, '请先登录');// 场景6:服务器内部错误// sendJsonResponse(null, 500, '服务器内部错误,请稍后重试');// 实际演示:让我们模拟一个"获取图书详情成功"的场景// 假设我们收到了请求 GET /api/books/1$requestedBookId=1;$foundBook=null;foreach($booksas$book){if($book['id']==$requestedBookId){$foundBook=$book;break;}}if($foundBook){sendJsonResponse($foundBook,200,'图书详情获取成功');}else{sendJsonResponse(null,404,"ID为{$requestedBookId}的图书不存在");}?>访问此脚本,预期输出为(因为找到了ID为1的图书):
{"success":true,"message":"图书详情获取成功","data":{"id":1,"title":"PHP从入门到精通","author":"张三"}}示例3:解析JSON请求体
当客户端(如前端)以JSON格式发送POST或PUT数据时,服务器端需要正确解析.
<?php// 示例:handle_json_request.php// 演示如何接收并处理客户端发送的JSON请求体// 设置响应为JSON格式header('Content-Type: application/json; charset=utf-8');// 只允许POST方法if($_SERVER['REQUEST_METHOD']!=='POST'){http_response_code(405);echojson_encode(['success'=>false,'message'=>'只支持POST方法']);exit;}// 1. 获取原始的POST数据(php://input是一个只读流,用于读取请求的原始数据)$jsonPayload=file_get_contents('php:// input');// 2. 检查是否收到了数据if(empty($jsonPayload)){http_response_code(400);echojson_encode(['success'=>false,'message'=>'请求体不能为空']);exit;}// 3. 将JSON字符串解码为PHP关联数组$requestData=json_decode($jsonPayload,true);// 第二个参数true得到数组,false得到对象// 4. 检查JSON解码是否成功if(json_last_error()!==JSON_ERROR_NONE){http_response_code(400);echojson_encode(['success'=>false,'message'=>'JSON格式无效: '.json_last_error_msg()]);exit;}// 5. 假设我们需要`title`和`author`字段if(!isset($requestData['title'])||!isset($requestData['author'])){http_response_code(422);// 422 Unprocessable Entity 常用于请求格式正确但语义错误(如缺少字段)echojson_encode(['success'=>false,'message'=>'缺少必要字段','required_fields'=>['title','author']]);exit;}// 6. 模拟"处理数据"(例如,存入数据库)// 这里我们只是简单地将数据返回,并模拟一个生成的ID$newBookId=rand(100,999);$processedData=['id'=>$newBookId,'title'=>htmlspecialchars($requestData['title']),// 简单过滤,防止XSS(后续章节详解)'author'=>htmlspecialchars($requestData['author']),'created_at'=>date('Y-m-d H:i:s')];// 7. 返回成功响应http_response_code(201);// 201 Createdechojson_encode(['success'=>true,'message'=>'图书创建成功','data'=>$processedData],JSON_UNESCAPED_UNICODE);?>如何使用Postman测试此API:
- URL:
http:// 你的域名/handle_json_request.php - 方法:
POST - 头部(Headers): 添加
Content-Type: application/json - 体(Body): 选择
raw,然后输入JSON:
{"title":"HTTP权威指南","author":"David Gourley"}- 点击发送,预期收到类似下面的响应(ID是随机生成的):
{"success":true,"message":"图书创建成功","data":{"id":456,"title":"HTTP权威指南","author":"David Gourley","created_at":"2023-10-27 15:30:00"}}实战项目:图书管理系统API设计
项目需求分析
我们将为一个简单的"图书管理系统"设计并模拟实现一套符合RESTful风格的API.该系统需要管理图书信息,每本图书包含以下属性:
id(整数, 唯一标识, 通常由数据库自动生成)title(字符串, 书名)author(字符串, 作者)isbn(字符串, 国际标准书号, 可选)published_year(整数, 出版年份, 可选)created_at(时间戳, 创建时间)
功能需求:
- 查看图书列表:支持分页和简单筛选(如按作者).
- 查看单本图书详情.
- 添加新图书.
- 更新图书信息(支持完整更新和部分更新).
- 删除图书.
API接口设计文档
遵循RESTful规范,设计以下接口:
| 功能 | HTTP方法 | 端点(URI) | 描述 | 成功状态码 |
|---|---|---|---|---|
| 获取图书列表 | GET | /api/books | 获取所有图书,支持查询参数?author=XXX&page=1&limit=10 | 200 OK |
| 获取图书详情 | GET | /api/books/{id} | 获取指定ID的图书详情 | 200 OK |
| 创建图书 | POST | /api/books | 创建一本新图书,数据在请求体(JSON)中 | 201 Created |
| 完整更新图书 | PUT | /api/books/{id} | 更新指定ID的图书(需提供所有字段) | 200 OK |
| 部分更新图书 | PATCH | /api/books/{id} | 更新指定ID的图书(仅提供需修改的字段) | 200 OK |
| 删除图书 | DELETE | /api/books/{id} | 删除指定ID的图书 | 204 No Content |
分步骤实现与说明
我们将创建一个PHP文件来模拟实现这个API(暂不连接真实数据库,使用数组模拟数据存储).
步骤1:创建项目结构并初始化模拟数据
<?php// 项目文件:book_rest_api.php// 这是一个简化的、单一文件的RESTful API示例,用于演示核心概念.// === 第1部分:配置与初始化 ===// 设置响应头为JSON,并允许跨域(方便本地前端测试,第6章详解)header('Content-Type: application/json; charset=utf-8');header('Access-Control-Allow-Origin: *');// 在生产环境中应限制为具体域名header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');header('Access-Control-Allow-Headers: Content-Type');// 如果是OPTIONS请求(CORS预检请求),直接返回200if($_SERVER['REQUEST_METHOD']==='OPTIONS'){http_response_code(200);exit;}// 启用错误报告(开发环境)error_reporting(E_ALL);ini_set('display_errors',1);// 模拟一个"数据库" - 使用全局数组$books=[1=>['id'=>1,'title'=>'深入理解计算机系统','author'=>'Randal E.Bryant','isbn'=>'9787111321330','published_year'=>2010],2=>['id'=>2,'title'=>'代码整洁之道','author'=>'Robert C. Martin','isbn'=>'9787121177407','published_year'=>2011],3=>['id'=>3,'title'=>'HTTP权威指南','author'=>'David Gourley','isbn'=>'9787115281487','published_year'=>2012],];$nextId=4;// 用于模拟自增ID// 辅助函数:发送JSON响应functionsendJson($data,$statusCode=200,$message=''){http_response_code($statusCode);$response=['success'=>$statusCode>=200&&$statusCode<300];if($message){$response['message']=$message;}$response['data']=$data;echojson_encode($response,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);exit;}// 获取当前请求的HTTP方法$method=$_SERVER['REQUEST_METHOD'];// 解析请求路径.在实际框架中,这部分由路由组件完成.$path=parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH);// 我们简单地从路径中提取ID.更复杂的路由需要正则匹配.$pathParts=explode('/',$path);$requestedId=null;if(isset($pathParts[3])&&is_numeric($pathParts[3])){// 假设路径是 /api/books/1$requestedId=(int)$pathParts[3];}?>步骤2:实现路由分发(根据HTTP方法和路径决定执行什么操作)
<?php// === 第2部分:路由分发与核心逻辑 ===// 注意:这是一个非常简化的路由.真实项目应使用框架的路由器.// 定义资源基础路径$basePath='/api/books';// 检查请求是否针对我们的APIif(strpos($path,$basePath)===0){// 处理 /api/books 集合资源if($path===$basePath||$path===$basePath.'/'){handleBookCollection($method,$books,$nextId);}// 处理 /api/books/{id} 单个资源elseif($requestedId!==null){handleSingleBook($method,$requestedId,$books,$nextId);}else{sendJson(null,404,'接口路径不存在');}}else{sendJson(null,404,'欢迎使用图书管理API,请访问 /api/books');}// === 第3部分:具体请求处理函数 ===/** * 处理针对图书集合(/api/books)的请求 */functionhandleBookCollection($method,&$books,&$nextId){global$books,$nextId;// 使用全局变量模拟数据存储(实际应用中应操作数据库)switch($method){case'GET':// 获取图书列表// 模拟分页和过滤$authorFilter=$_GET['author']??null;$filteredBooks=array_values($books);// 获取所有书籍的值if($authorFilter){$filteredBooks=array_filter($filteredBooks,function($book)use($authorFilter){returnstripos($book['author'],$authorFilter)!==false;});$filteredBooks=array_values($filteredBooks);// 重置索引}sendJson($filteredBooks,200,'图书列表获取成功');break;case'POST':// 创建新图书$inputData=json_decode(file_get_contents('php:// input'),true);if(json_last_error()!==JSON_ERROR_NONE){sendJson(null,400,'无效的JSON格式');}// 验证必要字段if(empty($inputData['title'])||empty($inputData['author'])){sendJson(null,422,'缺少必要字段:title 和 author 是必填的');}// 创建新图书对象$newBook=['id'=>$nextId,'title'=>htmlspecialchars($inputData['title']),'author'=>htmlspecialchars($inputData['author']),'isbn'=>htmlspecialchars($inputData['isbn']??''),'published_year'=>isset($inputData['published_year'])?(int)$inputData['published_year']:null,'created_at'=>date('Y-m-d H:i:s')];// "保存"到模拟数据库$books[$nextId]=$newBook;$nextId++;sendJson($newBook,201,'图书创建成功');break;default:// 不允许的方法header('Allow: GET, POST');sendJson(null,405,'不允许的请求方法');break;}}/** * 处理针对单本图书(/api/books/{id})的请求 */functionhandleSingleBook($method,$id,&$books,&$nextId){global$books;// 检查图书是否存在if(!isset($books[$id])){sendJson(null,404,"ID为{$id}的图书不存在");}$book=&$books[$id];// 引用,便于直接修改switch($method){case'GET':// 获取图书详情sendJson($book,200,'图书详情获取成功');break;case'PUT':// 完整更新 - 需要提供所有字段$inputData=json_decode(file_get_contents('php:// input'),true);if(json_last_error()!==JSON_ERROR_NONE){sendJson(null,400,'无效的JSON格式');}// 验证所有必需字段(根据你的业务逻辑定义)$requiredFields=['title','author'];foreach($requiredFieldsas$field){if(!isset($inputData[$field])){sendJson(null,422,"字段 '{$field}' 是必填的(PUT要求完整更新)");}}// 执行完整更新$book['title']=htmlspecialchars($inputData['title']);$book['author']=htmlspecialchars($inputData['author']);$book['isbn']=htmlspecialchars($inputData['isbn']??$book['isbn']);// 保持原值或更新$book['published_year']=isset($inputData['published_year'])?(int)$inputData['published_year']:$book['published_year'];$book['updated_at']=date('Y-m-d H:i:s');// 添加更新时间戳sendJson($book,200,'图书信息已完整更新');break;case'PATCH':// 部分更新 - 只更新提供的字段$inputData=json_decode(file_get_contents('php:// input'),true);if(json_last_error()!==JSON_ERROR_NONE){sendJson(null,400,'无效的JSON格式');}// 如果没有提供任何可更新的字段$updatableFields=['title','author','isbn','published_year'];$hasUpdate=false;foreach($updatableFieldsas$field){if(array_key_exists($field,$inputData)){// 使用array_key_exists,因为值可能为null$book[$field]=is_string($inputData[$field])?htmlspecialchars($inputData[$field]):$inputData[$field];$hasUpdate=true;}}if(!$hasUpdate){sendJson(null,400,'未提供任何可更新的字段');}$book['updated_at']=date('Y-m-d H:i:s');sendJson($book,200,'图书信息已部分更新');break;case'DELETE':// 删除图书unset($books[$id]);// 返回204 No Content,通常没有响应体http_response_code(204);// 可以输出一个空响应,或者直接exitexit;break;default:header('Allow: GET, PUT, PATCH, DELETE');sendJson(null,405,'不允许的请求方法');break;}}?>项目测试指南
使用Postman对实现的API进行全面测试:
测试1:获取所有图书
- 方法:
GET - URL:
http:// 你的域名/book_rest_api.php/api/books - 预期响应:包含3本初始图书的JSON数组,状态码200.
测试2:按作者过滤图书
- 方法:
GET - URL:
http:// 你的域名/book_rest_api.php/api/books?author=David - 预期响应:仅包含作者名包含"David"的图书.
测试3:获取单本图书详情
- 方法:
GET - URL:
http:// 你的域名/book_rest_api.php/api/books/2 - 预期响应:ID为2的图书详情,状态码200.
测试4:创建新图书(POST)
- 方法:
POST - URL:
http:// 你的域名/book_rest_api.php/api/books - 头部:
Content-Type: application/json - Body(raw JSON):
{"title":"设计模式:可复用面向对象软件的基础","author":"Erich Gamma","isbn":"9787111618331","published_year":2019}- 预期响应:状态码201,响应体中包含新创建的图书信息(带有新ID).
测试5:完整更新图书(PUT)
- 方法:
PUT - URL:
http:// 你的域名/book_rest_api.php/api/books/1(更新第一本书) - 头部:
Content-Type: application/json - Body:
{"title":"深入理解计算机系统(原书第3版)","author":"Randal E.Bryant & David R.O'Hallaron","isbn":"9787111544937","published_year":2016}- 预期响应:状态码200,返回更新后的完整图书信息.随后用GET请求验证.
测试6:部分更新图书(PATCH)
- 方法:
PATCH - URL:
http:// 你的域名/book_rest_api.php/api/books/1 - 头部:
Content-Type: application/json - Body:
{"published_year":2022}- 预期响应:状态码200,返回的图书信息中仅
published_year和updated_at字段被修改.
测试7:删除图书(DELETE)
- 方法:
DELETE - URL:
http:// 你的域名/book_rest_api.php/api/books/3 - 预期响应:状态码204,无响应体.随后用GET
/api/books或 GET/api/books/3验证已删除(应返回404).
测试8:错误处理测试
- 获取不存在的图书:
GET /api/books/999-> 应返回404. - 用错误方法请求:
POST /api/books/1-> 应返回405,并在Allow头中告知允许的方法. - 发送无效JSON:
POST /api/books但Body是{invalid json-> 应返回400. - 创建时缺少必填字段:
POST /api/books只提供{"title": "书"}-> 应返回422.
项目部署入门(简易)
为了让你的API能被他人(或你的前端页面)通过网络访问,你需要将其部署到服务器.这里提供一个最基础的本地模拟部署概念:
本地服务器:使用你在第1章安装的XAMPP或PHPStudy.
- 将
book_rest_api.php文件放入Web服务器的根目录(如XAMPP的htdocs文件夹). - 确保Apache服务正在运行.
- 在浏览器或Postman中,通过
http:// localhost/book_rest_api.php/api/books即可访问你的API.
- 将
虚拟主机/云服务器(概念):
- 购买一个域名和虚拟主机(支持PHP和MySQL).
- 使用FTP工具(如FileZilla)将你的PHP文件上传到主机商指定的目录(通常是
public_html或htdocs). - 通过你的域名(如
http:// yourdomain.com/book_rest_api.php/api/books)访问API. - 注意:生产环境需要考虑安全性(关闭错误显示)、性能、数据库连接等,这将在第6章进一步探讨.
最佳实践
RESTful API设计规范总结
- 使用名词,而非动词:URI标识资源,HTTP动词定义操作.
- 正确使用HTTP状态码:这是API与客户端沟通的"语言",务必准确.
- 版本化你的API:将版本号(如
v1)纳入URI(/api/v1/books)或通过请求头(如Accept: application/vnd.myapp.v1+json)管理.这为未来不兼容的变更提供回旋余地. - 提供清晰的文档:使用工具如Swagger/OpenAPI自动生成API文档,或至少维护一个清晰的Markdown文档.
- 一致的命名约定:URI使用小写字母、连字符
-分隔单词.字段名使用蛇形命名法(snake_case)或驼峰命名法(camelCase),但整个API应保持一致. - 使用合适的HTTP方法:牢记GET(读)、POST(创)、PUT(全更新)、PATCH(部分更新)、DELETE(删)的语义.
- 合理的分页、过滤和排序:对于返回列表的接口(如
GET /api/books),必须支持分页(?page=1&limit=20)以避免返回海量数据.同时提供过滤(?author=xxx)和排序(?sort=-created_at)参数. - 返回标准化的响应格式:如本章示例所示,使用一个包含
success,message,data等字段的包装结构,使错误处理标准化.
常见错误与避坑指南
- 错误1:用GET方法执行修改操作.
- 现象:
GET /api/books/delete/1. - 问题:GET是安全且幂等的,可能被浏览器预加载、缓存,导致意外删除.搜索引擎爬虫也可能触发此操作.
- 修正:使用
DELETE /api/books/1.
- 现象:
- 错误2:忽略状态码,所有响应都返回200.
- 现象:即使资源未找到(404)或服务器错误(500),也返回
{"code": 404, "message": "..."},但HTTP状态码是200. - 问题:破坏了HTTP协议语义,客户端(如浏览器、负载均衡器)无法根据状态码进行标准处理(如重试、缓存、错误页面).
- 修正:永远优先使用正确的HTTP状态码,然后在响应体中提供更详细的错误信息.
- 现象:即使资源未找到(404)或服务器错误(500),也返回
- 错误3:在URI中使用动词.
- 现象:
/api/getBooks,/api/createUser. - 问题:不符合RESTful风格,URI冗长且不直观.
- 修正:使用资源名+HTTP方法:
GET /api/books,POST /api/users.
- 现象:
- 错误4:不处理跨域请求(CORS).
- 现象:当你的前端页面(运行在
http:// localhost:3000)尝试调用后端API(http://localhost:80)时,浏览器控制台报CORS错误. - 问题:浏览器的同源策略阻止了跨域请求.
- 修正:在API的响应头中添加
Access-Control-Allow-Origin等CORS头(如本章示例所示).更安全的做法是在生产环境中指定确切的来源域名.
- 现象:当你的前端页面(运行在
安全性考虑与漏洞案例(针对本章知识)
虽然全面的API安全(如JWT认证、SQL注入防护)将在第5章深入讲解,但基于HTTP协议和基本开发,现在就需要建立安全意识.
案例:缺少输入验证导致潜在XSS(跨站脚本攻击)
- 漏洞场景:在
POST /api/books接口中,我们接收title和author字段,但未对内容进行任何过滤,直接存储并可能在管理页面上显示. - 攻击模拟:
- 攻击者发送一个创建图书的请求:
{"title":"一本好书<script>alert('XSS攻击')</script>","author":"黑客"}- 如果后端直接存储了这个`title`,并且前端在显示图书列表时未做转义,直接使用`innerHTML`,那么任何查看图书列表的用户浏览器都会执行`alert('XSS攻击')`脚本. - 更危险的脚本可以窃取用户的Cookie(如果未设置HttpOnly)或进行其他恶意操作.- 防护代码(基础):
<?php// 在接收用户输入后,进行基本的HTML转义// 使用 htmlspecialchars 函数$title=htmlspecialchars($inputData['title'],ENT_QUOTES,'UTF-8');$author=htmlspecialchars($inputData['author'],ENT_QUOTES,'UTF-8');// 存入数据库的是转义后的安全文本// 当这些数据被输出到HTML页面时,它们将被视为纯文本,而不是可执行的HTML/JS代码.?>**注意**:`htmlspecialchars`是输出到HTML上下文时的防护措施.对于API,更关键的是要明确数据的用途.如果API只是将数据返回给客户端,而客户端决定如何渲染(是作为HTML还是纯文本),那么API端进行转义可能不总是合适的.**最佳实践是"在使用的上下文进行编码"**.对于API,应通过文档告知客户端哪些字段可能包含用户输入,建议前端在渲染时自行转义.但对于简单的演示和防止自身管理界面被攻击,在入库前转义也是一种防御深度.案例:不安全的直接文件访问
- 漏洞场景:你的API文件
book_rest_api.php可能包含数据库密码等敏感配置信息(虽然我们示例中没用). - 攻击模拟:攻击者直接访问
http:// yoursite.com/config.php. - 防护:
- 将配置文件放在Web根目录之外.
- 在包含敏感信息的PHP文件开头添加
if (!defined('IN_APP')) { die('Access Denied'); },并通过一个入口文件(如index.php)定义常量后包含它. - 配置Web服务器(如Apache的
.htaccess)阻止对特定文件的直接访问.
练习题与挑战
基础练习题
- 题目:HTTP状态码分类
- 描述:请列出5个常见的HTTP状态码(每个类别选一个:2xx, 3xx, 4xx, 4xx, 5xx),并分别简要说明其含义和典型应用场景.
- 难度:★☆☆☆☆
- 提示:参考本章"常见HTTP状态码"表格.
- 参考答案:
- 200 OK:请求成功.一般用于GET、PUT、PATCH请求成功后的响应.
- 301 Moved Permanently:请求的资源已被永久移动到新URI.浏览器会自动重定向到新地址.
- 400 Bad Request:客户端请求有语法错误,服务器无法理解.常用于参数格式错误.
- 404 Not Found:服务器无法找到请求的资源.用于URI指向不存在的资源.
- 500 Internal Server Error:服务器内部错误,无法完成请求.用于未捕获的服务器端异常.
- 参考答案:
- 题目:RESTful URI设计
- 描述:为一个"博客系统"设计满足以下功能的RESTful API URI(只写URI,不写方法):
1. 获取所有文章列表.
2. 获取ID为5的文章详情.
3. 创建一篇新文章.
4. 更新ID为5的文章的标题和内容.
5. 删除ID为5的文章.
6. 获取ID为5的文章下的所有评论.
7. 为ID为5的文章添加一条新评论.- 难度:★★☆☆☆
- 提示:资源是"文章"和"评论",注意层级关系.
- 参考答案:
/api/articles/api/articles/5/api/articles/api/articles/5(使用PUT或PATCH方法)/api/articles/5/api/articles/5/comments/api/articles/5/comments
- 参考答案:
进阶练习题
- 题目:用PHP读取并响应HTTP请求头
- 描述:编写一个PHP脚本
check_headers.php.当用户访问时,该脚本应检查请求头中是否包含Accept头,并且其值包含application/json.如果包含,则返回JSON格式的响应{"message": "你接受JSON格式"},状态码200;如果不包含或值不是JSON,则返回纯文本响应"请使用接受JSON格式的客户端(如Postman)访问",状态码406(Not Acceptable).- 难度:★★★☆☆
- 提示:使用
$_SERVER[‘HTTP_ACCEPT’]获取Accept请求头.使用strpos函数判断是否包含application/json.使用http_response_code和header函数设置状态码和内容类型.- 参考答案:
<?php$acceptHeader=$_SERVER['HTTP_ACCEPT']??'';if(strpos($acceptHeader,'application/json')!==false){header('Content-Type: application/json');http_response_code(200);echojson_encode(['message'=>'你接受JSON格式']);}else{header('Content-Type: text/plain; charset=utf-8');http_response_code(406);echo'请使用接受JSON格式的客户端(如Postman)访问';}?>- 题目:模拟PATCH请求的部分更新逻辑
- 描述:基于本章的
book_rest_api.php示例,假设现在图书资源新增了一个price(价格,浮点数)字段.请补充handleSingleBook函数中PATCH方法的处理逻辑,使其能够正确地仅更新客户端提供的price字段(如果提供了的话),同时保持其他字段不变.需要考虑如果提供的price不是有效数字的处理.- 难度:★★★☆☆
- 提示:扩展
$updatableFields数组,并在更新price前使用is_numeric或filter_var进行验证.- 参考答案(补充部分):
// 在handleSingleBook函数的PATCH case中,修改如下:$updatableFields=['title','author','isbn','published_year','price'];$hasUpdate=false;foreach($updatableFieldsas$field){if(array_key_exists($field,$inputData)){// 特别处理price字段if($field==='price'){if(!is_numeric($inputData['price'])){sendJson(null,422,"字段 'price' 必须是有效的数字");}$book[$field]=(float)$inputData['price'];}else{$book[$field]=is_string($inputData[$field])?htmlspecialchars($inputData[$field]):$inputData[$field];}$hasUpdate=true;}}综合挑战题
- 题目:设计并模拟实现"用户收藏"API
- 描述:在"图书管理系统"的基础上,扩展功能,允许用户(我们先假设用户已通过某种方式认证,用户ID为
user_id)收藏图书.你需要设计新的API接口,并修改book_rest_api.php,用一个模拟数组$userFavorites(键为用户ID,值为该用户收藏的图书ID数组)来模拟此功能. - 需求:
- 设计接口:设计"用户收藏"相关的RESTful API(方法、URI、描述).考虑如何表示"用户1收藏图书2"这个关系.
- 实现功能:在现有代码中,至少实现两个核心接口:
POST /api/users/{user_id}/favorites:用户收藏一本图书.请求体需包含book_id.需检查图书是否存在,以及是否已收藏.GET /api/users/{user_id}/favorites:获取用户收藏的所有图书详情列表(需要从$books中取出对应ID的完整图书信息).
- 错误处理:对重复收藏、收藏不存在的图书等情况返回合适的状态码和错误信息.
- 难度:★★★★☆
- 描述:在"图书管理系统"的基础上,扩展功能,允许用户(我们先假设用户已通过某种方式认证,用户ID为
- 提示:这是一个"用户"和"图书"之间的多对多关系.在设计URI时,可以将其视为用户的子资源
favorites.实现时,注意修改全局模拟数据数组.- 参考答案(部分核心逻辑):
<?php// 新增模拟数据$userFavorites=[1=>[2,3],// 用户1收藏了图书2和3];// 在路由分发部分,需要新增对`/api/users/{id}/favorites`路径的判断和相应处理函数调用.// 处理函数示例片段:functionhandleUserFavorites($method,$userId,&$userFavorites,&$books){$userId=(int)$userId;// 初始化用户收藏夹(如果不存在)if(!isset($userFavorites[$userId])){$userFavorites[$userId]=[];}switch($method){case'GET':$favoriteBooks=[];foreach($userFavorites[$userId]as$bookId){if(isset($books[$bookId])){$favoriteBooks[]=$books[$bookId];}}sendJson($favoriteBooks,200,'获取收藏列表成功');break;case'POST':$input=json_decode(file_get_contents('php:// input'),true);$bookId=$input['book_id']??null;if(!$bookId||!is_numeric($bookId)){sendJson(null,400,'必须提供有效的book_id');}$bookId=(int)$bookId;// 检查图书是否存在if(!isset($books[$bookId])){sendJson(null,404,"图书不存在");}// 检查是否已收藏if(in_array($bookId,$userFavorites[$userId])){sendJson(null,409,'该图书已收藏');// 409 Conflict}// 执行收藏$userFavorites[$userId][]=$bookId;sendJson(['user_id'=>$userId,'book_id'=>$bookId],201,'收藏成功');break;// 可以继续实现DELETE取消收藏等default:header('Allow: GET, POST, DELETE');sendJson(null,405,'不允许的请求方法');}}?>章节总结
本章重点知识回顾
- HTTP协议是Web通信的基石:理解了客户端-服务器模型、无状态、请求/响应报文结构.
- HTTP动词定义操作:GET(取)、POST(创)、PUT(全更新)、PATCH(部分更新)、DELETE(删),并理解了幂等性与安全性的概念.
- HTTP状态码是结果的语言:熟练掌握了2xx(成功)、4xx(客户端错误)、5xx(服务器错误)系列中的常见状态码及其应用场景.
- 头部(Headers)携带元数据:掌握了关键的
Content-Type,Accept,Authorization等头部的用法. - RESTful是一种优雅的设计风格:学会了以资源为中心设计URI,使用标准的HTTP方法操作资源,使API直观、易用、符合行业规范.
- JSON是现代API的数据通用语:学会了在PHP中使用
json_encode和json_decode处理JSON数据.
技能掌握要求
学完本章后,你应该能够:
- 解读:使用浏览器开发者工具或Postman,分析任何一个网站的HTTP请求与响应,理解其使用的方-法、状态码和头部信息.
- 设计:为一个新的业务需求(如"商品管理"、“订单系统”)设计出一套符合RESTful风格的API接口文档.
- 实现:在不依赖框架的情况下,使用PHP编写能够处理不同HTTP方法、接收JSON数据、返回标准JSON响应并设置正确状态码的简单API端点.
- 测试:熟练使用Postman等工具,对你设计的API进行全面的测试,包括成功用例和各类错误用例.
进一步学习建议
- 深入了解HTTP:阅读RFC 7230等HTTP/1.1标准文档的部分章节,或阅读《HTTP权威指南》一书,以更深入地理解协议细节(如缓存、连接管理).
- 探索API设计进阶话题:学习HATEOAS(超媒体作为应用状态引擎)、GraphQL作为一种替代REST的API查询语言.
- 准备进入实战:你已经掌握了构建API所需的所有"理论规则".下一章,我们将把这些规则与第2章的数据库知识结合起来,真正动手构建一个连接数据库的、完整的API项目.建议你在学习第4章前,复习一下第2章的PDO数据库操作,并确保你的开发环境已准备好MySQL数据库.
恭喜你!你已经理解了Web世界最核心的"对话规则".现在,是时候开始用代码编写属于你自己的"对话"了.让我们进入第4章,开启API构建之旅!