第3章:对话规则:理解HTTP协议与RESTful API设计

章节介绍

学习目标

通过本章学习,你将能够:

  1. 深刻理解HTTP协议在Web通信中的核心作用.
  2. 掌握关键HTTP方法(GET, POST, PUT, PATCH, DELETE)的语义与正确使用场景.
  3. 解读常见HTTP状态码(如200, 404, 500)的含义,并能在API中正确应用.
  4. 理解请求头与响应头(如Content-Type, Authorization)的作用并熟练使用.
  5. 掌握RESTful架构风格的核心思想与设计规范.
  6. 熟练使用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.它强调:

  1. 以资源为中心:将服务器提供的数据/服务抽象为"资源"(Resource),如用户、文章、订单.每个资源有唯一的标识符(URI).
  2. 统一的接口:使用标准的HTTP方法(GET/POST/PUT/DELETE等)来操作资源.
  3. 无状态性:每次请求都包含处理该请求所需的所有信息,服务器不保存会话状态.这提高了可扩展性和可靠性.
  4. 可缓存:响应应被明确标记为可缓存或不可缓存,以提高性能.
  5. 分层系统:客户端通常不知道是否直接连接到最终服务器,还是中间的代理(如负载均衡器、缓存服务器).

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-8
header('Content-Type: text/plain; charset=utf-8');

// 5. 根据请求方法做出不同响应
switch ($requestMethod) {
    case 'GET':
        // 返回一个简单的问候信息
$responseBody = "你好,{$name}!";
        if ($age > 0) {
            $responseBody .= " 听说你{$age}岁了.";
        }
        // 设置HTTP状态码为200 OK
        http_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 可选的错误详情数组
*/
function sendJsonResponse($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字符串并输出
echo json_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 ($books as $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);
    echo json_encode(['success' => false, 'message' => '只支持POST方法']);
    exit;
}

// 1. 获取原始的POST数据(php://input是一个只读流,用于读取请求的原始数据)
$jsonPayload = file_get_contents('php:// input');

// 2. 检查是否收到了数据
if (empty($jsonPayload)) {
    http_response_code(400);
    echo json_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);
    echo json_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 常用于请求格式正确但语义错误(如缺少字段)
    echo json_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 Created
echo json_encode([
    'success' => true,
    'message' => '图书创建成功',
    'data' => $processedData
], JSON_UNESCAPED_UNICODE);
?>

如何使用Postman测试此API:

  1. URL: http:// 你的域名/handle_json_request.php
  2. 方法: POST
  3. 头部(Headers): 添加 Content-Type: application/json
  4. 体(Body): 选择raw,然后输入JSON:
    {
        "title": "HTTP权威指南",
        "author": "David Gourley"
    }
  1. 点击发送,预期收到类似下面的响应(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 (时间戳, 创建时间)

功能需求:

  1. 查看图书列表:支持分页和简单筛选(如按作者).
  2. 查看单本图书详情.
  3. 添加新图书.
  4. 更新图书信息(支持完整更新和部分更新).
  5. 删除图书.

API接口设计文档

遵循RESTful规范,设计以下接口:

功能HTTP方法端点(URI)描述成功状态码
获取图书列表GET/api/books获取所有图书,支持查询参数?author=XXX&page=1&limit=10200 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预检请求),直接返回200
if ($_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响应
function sendJson($data, $statusCode = 200, $message = '') {
    http_response_code($statusCode);
    $response = ['success' => $statusCode >= 200 && $statusCode < 300];
    if ($message) {
        $response['message'] = $message;
    }
    $response['data'] = $data;
    echo json_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';

// 检查请求是否针对我们的API
if (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)的请求
*/
function handleBookCollection($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) {
                    return stripos($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})的请求
*/
function handleSingleBook($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 ($requiredFields as $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 ($updatableFields as $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);
            // 可以输出一个空响应,或者直接exit
            exit;
            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_yearupdated_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. 本地服务器:使用你在第1章安装的XAMPP或PHPStudy.

    • book_rest_api.php 文件放入Web服务器的根目录(如XAMPP的htdocs文件夹).
    • 确保Apache服务正在运行.
    • 在浏览器或Postman中,通过 http:// localhost/book_rest_api.php/api/books 即可访问你的API.
  2. 虚拟主机/云服务器(概念):

    • 购买一个域名和虚拟主机(支持PHP和MySQL).
    • 使用FTP工具(如FileZilla)将你的PHP文件上传到主机商指定的目录(通常是public_htmlhtdocs).
    • 通过你的域名(如 http:// yourdomain.com/book_rest_api.php/api/books)访问API.
    • 注意:生产环境需要考虑安全性(关闭错误显示)、性能、数据库连接等,这将在第6章进一步探讨.

最佳实践

RESTful API设计规范总结

  1. 使用名词,而非动词:URI标识资源,HTTP动词定义操作.
  2. 正确使用HTTP状态码:这是API与客户端沟通的"语言",务必准确.
  3. 版本化你的API:将版本号(如v1)纳入URI(/api/v1/books)或通过请求头(如Accept: application/vnd.myapp.v1+json)管理.这为未来不兼容的变更提供回旋余地.
  4. 提供清晰的文档:使用工具如Swagger/OpenAPI自动生成API文档,或至少维护一个清晰的Markdown文档.
  5. 一致的命名约定:URI使用小写字母、连字符-分隔单词.字段名使用蛇形命名法(snake_case)或驼峰命名法(camelCase),但整个API应保持一致.
  6. 使用合适的HTTP方法:牢记GET(读)、POST(创)、PUT(全更新)、PATCH(部分更新)、DELETE(删)的语义.
  7. 合理的分页、过滤和排序:对于返回列表的接口(如GET /api/books),必须支持分页(?page=1&limit=20)以避免返回海量数据.同时提供过滤(?author=xxx)和排序(?sort=-created_at)参数.
  8. 返回标准化的响应格式:如本章示例所示,使用一个包含success, message, data等字段的包装结构,使错误处理标准化.

常见错误与避坑指南

  1. 错误1:用GET方法执行修改操作.
    • 现象: GET /api/books/delete/1.
    • 问题:GET是安全且幂等的,可能被浏览器预加载、缓存,导致意外删除.搜索引擎爬虫也可能触发此操作.
    • 修正:使用 DELETE /api/books/1.
  2. 错误2:忽略状态码,所有响应都返回200.
    • 现象:即使资源未找到(404)或服务器错误(500),也返回 {"code": 404, "message": "..."},但HTTP状态码是200.
    • 问题:破坏了HTTP协议语义,客户端(如浏览器、负载均衡器)无法根据状态码进行标准处理(如重试、缓存、错误页面).
    • 修正:永远优先使用正确的HTTP状态码,然后在响应体中提供更详细的错误信息.
  3. 错误3:在URI中使用动词.
    • 现象: /api/getBooks, /api/createUser.
    • 问题:不符合RESTful风格,URI冗长且不直观.
    • 修正:使用资源名+HTTP方法: GET /api/books, POST /api/users.
  4. 错误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接口中,我们接收titleauthor字段,但未对内容进行任何过滤,直接存储并可能在管理页面上显示.
  • 攻击模拟:
    • 攻击者发送一个创建图书的请求:
        {
            "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.
  • 防护:
    1. 将配置文件放在Web根目录之外.
    2. 在包含敏感信息的PHP文件开头添加if (!defined('IN_APP')) { die('Access Denied'); },并通过一个入口文件(如index.php)定义常量后包含它.
    3. 配置Web服务器(如Apache的.htaccess)阻止对特定文件的直接访问.

练习题与挑战

基础练习题

  1. 题目: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:服务器内部错误,无法完成请求.用于未捕获的服务器端异常.
  1. 题目:RESTful URI设计
  • 描述:为一个"博客系统"设计满足以下功能的RESTful API URI(只写URI,不写方法):
    1. 获取所有文章列表.
    2. 获取ID为5的文章详情.
    3. 创建一篇新文章.
    4. 更新ID为5的文章的标题和内容.
    5. 删除ID为5的文章.
    6. 获取ID为5的文章下的所有评论.
    7. 为ID为5的文章添加一条新评论.
    • 难度:★★☆☆☆
  • 提示:资源是"文章"和"评论",注意层级关系.
    • 参考答案:
      1. /api/articles
      2. /api/articles/5
      3. /api/articles
      4. /api/articles/5 (使用PUT或PATCH方法)
      5. /api/articles/5
      6. /api/articles/5/comments
      7. /api/articles/5/comments

进阶练习题

  1. 题目:用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_codeheader函数设置状态码和内容类型.
    • 参考答案:
        <?php
        $acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
        if (strpos($acceptHeader, 'application/json') !== false) {
            header('Content-Type: application/json');
            http_response_code(200);
            echo json_encode(['message' => '你接受JSON格式']);
        } else {
            header('Content-Type: text/plain; charset=utf-8');
            http_response_code(406);
            echo '请使用接受JSON格式的客户端(如Postman)访问';
        }
        ?>
  1. 题目:模拟PATCH请求的部分更新逻辑
  • 描述:基于本章的book_rest_api.php示例,假设现在图书资源新增了一个price(价格,浮点数)字段.请补充handleSingleBook函数中PATCH方法的处理逻辑,使其能够正确地仅更新客户端提供的price字段(如果提供了的话),同时保持其他字段不变.需要考虑如果提供的price不是有效数字的处理.
    • 难度:★★★☆☆
  • 提示:扩展$updatableFields数组,并在更新price前使用is_numericfilter_var进行验证.
    • 参考答案(补充部分):
        // 在handleSingleBook函数的PATCH case中,修改如下:
        $updatableFields = ['title', 'author', 'isbn', 'published_year', 'price'];
        $hasUpdate = false;
        foreach ($updatableFields as $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;
            }
        }

综合挑战题

  1. 题目:设计并模拟实现"用户收藏"API
    • 描述:在"图书管理系统"的基础上,扩展功能,允许用户(我们先假设用户已通过某种方式认证,用户ID为user_id)收藏图书.你需要设计新的API接口,并修改book_rest_api.php,用一个模拟数组$userFavorites(键为用户ID,值为该用户收藏的图书ID数组)来模拟此功能.
    • 需求:
      1. 设计接口:设计"用户收藏"相关的RESTful API(方法、URI、描述).考虑如何表示"用户1收藏图书2"这个关系.
      2. 实现功能:在现有代码中,至少实现两个核心接口:
        • POST /api/users/{user_id}/favorites:用户收藏一本图书.请求体需包含book_id.需检查图书是否存在,以及是否已收藏.
        • GET /api/users/{user_id}/favorites:获取用户收藏的所有图书详情列表(需要从$books中取出对应ID的完整图书信息).
      3. 错误处理:对重复收藏、收藏不存在的图书等情况返回合适的状态码和错误信息.
    • 难度:★★★★☆
  • 提示:这是一个"用户"和"图书"之间的多对多关系.在设计URI时,可以将其视为用户的子资源favorites.实现时,注意修改全局模拟数据数组.
    • 参考答案(部分核心逻辑):
        <?php
        // 新增模拟数据
$userFavorites = [
            1 => [2, 3], // 用户1收藏了图书2和3
        ];

        // 在路由分发部分,需要新增对`/api/users/{id}/favorites`路径的判断和相应处理函数调用.
        // 处理函数示例片段:
        function handleUserFavorites($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, '不允许的请求方法');
            }
        }
        ?>

章节总结

本章重点知识回顾

  1. HTTP协议是Web通信的基石:理解了客户端-服务器模型、无状态、请求/响应报文结构.
  2. HTTP动词定义操作:GET(取)、POST(创)、PUT(全更新)、PATCH(部分更新)、DELETE(删),并理解了幂等性与安全性的概念.
  3. HTTP状态码是结果的语言:熟练掌握了2xx(成功)、4xx(客户端错误)、5xx(服务器错误)系列中的常见状态码及其应用场景.
  4. 头部(Headers)携带元数据:掌握了关键的Content-Type, Accept, Authorization等头部的用法.
  5. RESTful是一种优雅的设计风格:学会了以资源为中心设计URI,使用标准的HTTP方法操作资源,使API直观、易用、符合行业规范.
  6. JSON是现代API的数据通用语:学会了在PHP中使用json_encodejson_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构建之旅!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐