CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
PHP Web 入门/课时/REST API 风格
RawBlame
课时目录12 节

PHP Web 入门

  1. 01PHP 是什么
  2. 02基础语法
  3. 03函数与命名空间
  4. 04面向对象
  5. 05Composer 依赖
  6. 06Web 请求处理
  7. 07PDO 与 MySQL
  8. 08REST API 风格
  9. 09PHP 8 新特性
  10. 10Docker 运行 PHP
  11. 11PHP vs Java 选型
  12. 12综合练习

REST API 风格

JSON 响应、状态码;对照 Spring Boot。

30 分钟

REST API 风格

这一课你将能做到

  1. 按 REST 语义设计资源路由:URL 定资源、方法定动作、状态码定结果
  2. 用 PHP 原生实现一个任务清单(Todo)API:GET/POST/PUT/DELETE 全覆盖
  3. 统一 JSON 响应结构与错误处理,并用 curl 对每个接口做验收测试

为什么要学

REST 是 HTTP 世界的「通用普通话」:前端 Vue/React、小程序、第三方系统、开放平台,全都通过「资源 + 方法 + 状态码」这套语义交互。PHP 上一课已经会用 PDO 操作数据,这一课把数据能力包成规范接口,你就打通了「前端请求 → 路由分发 → 业务处理 → 数据落库 → 统一响应」的完整链路——这正是 Laravel/Symfony 框架做的事,只是框架帮你写了路由器和分发器。

人话:REST 就是「把数据库里的东西当资源,用 HTTP 方法当动词」:GET 读、POST 建、PUT 全量改、DELETE 删;状态码 200/201/204/400/404/422/500 各说各话,别一律 200。

先建立图像

任务清单 API 的路由表(本课要实现的全部接口):

GET    /api/tasks          列出任务(支持 ?status=done 过滤)    200
POST   /api/tasks          新建任务(请求体 JSON)               201
GET    /api/tasks/{id}     查询单个任务                          200 / 404
PUT    /api/tasks/{id}     全量更新任务                          200 / 404
DELETE /api/tasks/{id}     删除任务                              204 / 404

约定:
URL 只有名词复数,动词交给 HTTP 方法
状态码表达结果,响应体统一 {"code": ..., "message": ..., "data": ...}

跟着做

步骤 1:路由分发器(前端控制器)

api.php——所有请求的唯一入口:

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');                    // 演示用 CORS

function respond(int $code, array $data = []): never {
    http_response_code($code);
    if ($code !== 204) {                                       // 204 无响应体
        echo json_encode($data + ['code' => $code], JSON_UNESCAPED_UNICODE);
    }
    exit;
}

$method = $_SERVER['REQUEST_METHOD'];
$uri    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);  // /api/tasks/3

// 从 /api/tasks/3 提取资源与 id
if (!str_starts_with($uri, '/api/tasks')) {
    respond(404, ['message' => '未知资源']);
}
$id = null;
if (preg_match('#^/api/tasks/(\d+)$#', $uri, $m)) {
    $id = (int) $m[1];
}

内置服务器支持路由脚本,让所有路径都进 api.php:php -S localhost:8000 api.php。

步骤 2:GET 列表与详情

<?php
// 沿用上一课的 $pdo 连接(真实代码 require db.php)
if ($method === 'GET' && $id === null) {
    $status = $_GET['status'] ?? null;                     // 过滤参数走查询串
    $sql    = 'SELECT id, title, status, created_at FROM tasks';
    $params = [];
    if ($status !== null) {
        $sql .= ' WHERE status = ?';
        $params[] = $status;
    }
    $sql .= ' ORDER BY id DESC LIMIT 20';
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    respond(200, ['data' => $stmt->fetchAll()]);
}

if ($method === 'GET' && $id !== null) {
    $stmt = $pdo->prepare('SELECT * FROM tasks WHERE id = ?');
    $stmt->execute([$id]);
    $task = $stmt->fetch();
    $task === false
        ? respond(404, ['message' => "任务 {$id} 不存在"])
        : respond(200, ['data' => $task]);
}

步骤 3:POST 创建与 JSON 请求体

<?php
if ($method === 'POST') {
    // $_POST 解析不了 JSON,必须读原始输入流
    $body = json_decode(file_get_contents('php://input'), true);
    if (!is_array($body)) {
        respond(400, ['message' => '请求体必须是合法 JSON']);
    }

    $title = trim((string) ($body['title'] ?? ''));
    if ($title === '' || mb_strlen($title) > 50) {
        respond(422, ['message' => 'title 必填且不超过 50 字']);   // 校验失败 422
    }

    $stmt = $pdo->prepare('INSERT INTO tasks (title, status) VALUES (?, ?)');
    $stmt->execute([$title, 'todo']);

    respond(201, [                                              // 创建成功 201
        'message' => '创建成功',
        'data'    => ['id' => (int) $pdo->lastInsertId()],
    ]);
}

步骤 4:PUT 更新与 DELETE 删除

<?php
if ($method === 'PUT' && $id !== null) {
    $body  = json_decode(file_get_contents('php://input'), true) ?? [];
    $title = trim((string) ($body['title'] ?? ''));
    $status = in_array($body['status'] ?? '', ['todo', 'doing', 'done'], true)
        ? $body['status'] : 'todo';                        // 枚举值必须白名单校验

    if ($title === '') {
        respond(422, ['message' => 'PUT 是全量更新,title 必填']);
    }
    $stmt = $pdo->prepare('UPDATE tasks SET title = ?, status = ? WHERE id = ?');
    $stmt->execute([$title, $status, $id]);
    $stmt->rowCount() === 0
        ? respond(404, ['message' => "任务 {$id} 不存在"])
        : respond(200, ['message' => '更新成功']);
}

if ($method === 'DELETE' && $id !== null) {
    $stmt = $pdo->prepare('DELETE FROM tasks WHERE id = ?');
    $stmt->execute([$id]);
    $stmt->rowCount() === 0
        ? respond(404, ['message' => "任务 {$id} 不存在"])
        : respond(204);                                    // 删除成功无响应体
}

respond(405, ['message' => '方法不被允许']);

步骤 5:curl 验收

curl -X POST localhost:8000/api/tasks -H "Content-Type: application/json" \
     -d '{"title": "学习 REST"}'          # 201 {"code":201,...,"data":{"id":1}}

curl "localhost:8000/api/tasks?status=todo"   # 200 列表
curl localhost:8000/api/tasks/1               # 200 详情
curl -X PUT localhost:8000/api/tasks/1 -H "Content-Type: application/json" \
     -d '{"title":"学习 REST","status":"done"}'   # 200
curl -X DELETE localhost:8000/api/tasks/1     # 204
curl localhost:8000/api/tasks/999             # 404

讲清楚要点

  1. REST 的三根柱子:名词化 URI 定位资源、HTTP 方法表达动作、状态码表达结果。反模式是 POST /api/deleteTask——动词进 URL、语义全靠自定义。
  2. 状态码有分工:200 成功返回体、201 已创建、204 成功无返回体、400 请求格式错、401 未认证、403 已认证无权限、404 资源不存在、405 方法不支持、422 语义校验失败、500 服务端错误。一律 200 + 错误码是常见坏味道,会破坏 HTTP 缓存与网关监控。
  3. 幂等性是 PUT/DELETE 的灵魂:GET/PUT/DELETE 幂等(重复调用结果一致),POST 不幂等(重复提交建多条)。前端「防重复点击」的 Retry 设计、网关重试策略都建立在幂等语义上。
  4. 统一响应结构:外层固定 code/message/data,code 与 HTTP 状态码保持同向。业务错误信息放 message,分页等元信息放 data 或 meta——这是前后端协作的契约,比返回结构自由发挥省掉大量沟通成本。

你可能会懵的地方

  • PUT 和 PATCH 的区别? PUT 是全量替换:客户端必须提交资源的完整表示,缺的字段会被覆盖;PATCH 是部分更新:只提交要改的字段。本课为简化用 PUT 承担了更新职责,规范做法是完整替换用 PUT、局部修改用 PATCH。
  • 为什么 URL 用复数名词? 资源集合的语义:GET /tasks 是「任务集合」,GET /tasks/3 是「集合中的第 3 个」。复数让集合与个体的 URI 天然一致,也让嵌套资源(/users/1/tasks)读起来自然。
  • CORS 是什么,后端要做什么? 浏览器同源策略限制跨域 JS 请求,服务端通过响应头授权:Access-Control-Allow-Origin(哪些来源可访问)、-Allow-Methods、-Allow-Headers;对非简单请求浏览器会先发 OPTIONS 预检,服务端需对 OPTIONS 返回 204 加 CORS 头。它只在浏览器生效,curl/Postman 不受限制。
  • 版本号放 URL 还是请求头? 都可,工程上 URL 更直观:/api/v1/tasks。大版本不兼容时新增 v2 而非改坏 v1,给客户端平滑迁移窗口。
  • 分页参数怎么设计? 通用做法 ?page=1&page_size=20(或游标 ?cursor=xxx),响应里带 meta: {page, page_size, total};page_size 服务端设上限(如 100),防止一次拖全表。

错法对照

错法问题正解
POST /api/delete?id=1动词进 URL,语义混乱DELETE /api/tasks/1
所有响应都返回 200监控、缓存、重试全部失灵按语义返回 201/204/404/422
直接 echo json_encode($row)无结构契约,前端难处理统一 code/message/data 外壳
用 $_POST 接 JSON 请求体$_POST 只解析表单编码读 php://input 再 json_decode
DELETE 返回 200 + 大段数据删除无返回体,204 即可成功删返回 204
PUT 只传部分字段未传字段被覆盖成默认值PUT 传全量,或改用 PATCH 语义

卡点急救

  1. php -S 下访问 /api/tasks/3 返回 404 找不到文件:内置服务器默认按文件找。用路由脚本启动:php -S localhost:8000 api.php,所有路径交给 api.php 处理。
  2. 前端跨域请求被浏览器拦截:服务端补 CORS 头;带 Cookie 时 Allow-Origin 不能为 *,要写具体域名并加 Allow-Credentials: true。
  3. PUT 后数据没变也不报 404:rowCount() 对「值没变化的 UPDATE」返回 0。需要区分「存在但值相同」时,先 SELECT 确认资源存在,再判断更新结果。

面试常问

  • REST 的核心原则有哪些? 资源化(URI 指向名词资源)、统一接口(HTTP 方法语义化且 GET/PUT/DELETE 幂等)、无状态(每个请求自包含,服务端不存会话上下文)、分层系统与可缓存。落地上表现为:名词复数 URI + 方法分动作 + 状态码表结果 + 统一 JSON 契约。
  • PUT 和 PATCH 的语义差异?为什么 PUT 是幂等的? PUT 全量替换资源表示,无论调用多少次,资源最终状态相同,故幂等;PATCH 局部修改,结果取决于当前资源状态,重复调用可能叠加(如 PATCH 自增字段),不保证幂等。
  • HTTP 状态码 401 与 403 的区别?422 与 400 的区别? 401 未认证(不知道你是谁),403 已认证但无权限(知道你是谁但不让你干);400 请求体语法/格式错误(JSON 解析失败),422 语法正确但语义校验失败(title 为空、status 非法)。选对状态码直接决定网关重试与监控告警的准确性。

自测三问

  1. 列出本课 5 个接口的「方法 + 路径 + 成功状态码」。
  2. 为什么 JSON API 要从 php://input 读请求体?$_POST 拿不到什么?
  3. GET、POST、PUT、DELETE 四个方法中谁不幂等?这对重试策略意味着什么?

小结一句话

REST = 名词 URI + 方法表动作 + 状态码表结果 + 统一 JSON 契约;PHP 原生百来行就能跑通一个合格 API,框架只是把这套骨架做得更顺手。

延伸

  • PHP 官方手册:HTTP 相关函数
  • 下一课:PHP 8 新特性——枚举、只读属性、纤程与 JIT。
加载本节练习…
← 上一课PDO 与 MySQL下一课 →PHP 8 新特性