1. 引言
在微服务与前后端分离日益普及的今天,REST API 已成为系统间通信的主流方式。PHP 凭借其部署简单、生态成熟的特点,依然是构建 Web 服务的常用选择。本文不引入 Laravel、Symfony 等重型框架,而是从零开始,用 PHP 原生能力配合轻量组件,搭建一个结构清晰、易于维护的 REST API 服务。
我们将依次覆盖 REST 设计原则、路由定义、请求解析、JSON 响应封装、状态码与错误处理,以及 API Key / Bearer Token 两种简单鉴权方式。整个项目不依赖 Composer 之外的第三方框架,代码可直接运行。
2. REST 设计原则简述
REST(Representational State Transfer)是一种基于 HTTP 协议的架构风格,核心思想是将一切视为资源,通过统一的接口对资源进行操作。理解以下原则,是设计好 API 的第一步。
2.1 资源与 URL 设计
资源是 REST 的核心抽象,URL 只用来定位资源,不包含动词。例如:
GET /api/users:获取用户列表POST /api/users:创建用户GET /api/users/42:获取单个用户PUT /api/users/42:整体更新用户DELETE /api/users/42:删除用户
资源名称使用复数名词,层级关系用斜杠表达,如 /api/users/42/orders 表示某个用户的订单列表。
2.2 HTTP 方法与语义
每个 HTTP 方法对应一种标准操作:
| 方法 | 语义 | 幂等 |
|---|---|---|
| GET | 查询资源 | 是 |
| POST | 创建资源 | 否 |
| PUT | 整体替换资源 | 是 |
| PATCH | 局部更新资源 | 否 |
| DELETE | 删除资源 | 是 |
幂等意味着多次执行结果一致,这对网络重试场景非常重要。
2.3 无状态与统一接口
服务端不保存客户端会话状态,每个请求都携带完整信息,这便于水平扩展。同时,所有资源通过统一的接口访问,使用标准状态码表达结果,使用标准媒体类型(如 JSON)表达数据。
3. 使用 FastRoute 定义路由
FastRoute 是 PHP 社区广泛使用的轻量路由库,性能优异,支持占位符与分组。首先通过 Composer 安装:
bash
composer require nikic/fast-route
3.1 基础路由配置
创建一个 routes.php 文件,集中定义所有路由:
php
<?php
// routes.php
use FastRoute\RouteCollector;
return function (RouteCollector $r) {
// 用户资源
$r->addRoute('GET', '/api/users', 'UserController@index');
$r->addRoute('POST', '/api/users', 'UserController@store');
$r->addRoute('GET', '/api/users/{id:\d+}', 'UserController@show');
$r->addRoute('PUT', '/api/users/{id:\d+}', 'UserController@update');
$r->addRoute('DELETE', '/api/users/{id:\d+}', 'UserController@destroy');
// 订单资源
$r->addRoute('GET', '/api/users/{id:\d+}/orders', 'OrderController@index');
};
{id:\d+} 是带正则约束的占位符,确保 id 只能是数字,避免非法参数进入业务层。
3.2 前端控制器(入口文件)
在 public/index.php 中统一调度:
php
<?php
// public/index.php
require __DIR__ . '/../vendor/autoload.php';
$dispatcher = FastRoute\simpleDispatcher(require __DIR__ . '/../routes.php');
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
// 去除查询字符串与末尾斜杠
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
// 404 处理
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
// 405 处理
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// 调用控制器
break;
}
4. 请求解析与 JSON 响应封装
4.1 请求体解析
REST API 通常接收 JSON 格式的请求体。我们需要一个统一的请求解析器:
php
<?php
// src/Http/Request.php
namespace App\Http;
class Request
{
public static function jsonBody(): array
{
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [];
}
return is_array($data) ? $data : [];
}
public static function queryParams(): array
{
return $_GET;
}
public static function header(string $name): ?string
{
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
return $_SERVER[$key] ?? null;
}
}
4.2 JSON 响应封装
统一的响应结构能让客户端解析逻辑保持一致。我们设计一个 Response 类:
php
<?php
// src/Http/Response.php
namespace App\Http;
class Response
{
public static function json(array $data, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
public static function success($data = null, string $message = 'ok'): void
{
self::json([
'code' => 0,
'message' => $message,
'data' => $data,
], 200);
}
public static function error(string $message, int $status, int $code = -1): void
{
self::json([
'code' => $code,
'message' => $message,
'data' => null,
], $status);
}
}
统一响应格式为 { code, message, data },其中 code 为业务码,HTTP 状态码 表达传输层结果,二者解耦,便于客户端分别处理。
5. 状态码与错误处理规范
5.1 常用状态码语义
| 状态码 | 含义 | 典型场景 |
|---|---|---|
| 200 | 成功 | GET 查询、PUT 更新 |
| 201 | 已创建 | POST 新建资源 |
| 204 | 无内容 | DELETE 删除成功 |
| 400 | 请求参数错误 | JSON 解析失败、校验不通过 |
| 401 | 未认证 | 缺少或无效的 Token |
| 403 | 无权限 | 已认证但无权访问 |
| 404 | 资源不存在 | 路由未匹配或资源缺失 |
| 405 | 方法不允许 | URL 存在但方法不支持 |
| 422 | 语义错误 | 业务校验失败 |
| 500 | 服务器内部错误 | 未捕获异常 |
5.2 全局异常处理
在入口文件中注册异常处理器,避免错误信息直接暴露给客户端:
php
<?php
// public/index.php 中追加
set_exception_handler(function (Throwable $e) {
error_log($e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
Response::error('服务器内部错误', 500);
});
生产环境务必隐藏堆栈细节,仅记录到日志;开发环境可选择性输出调试信息。
5.3 路由未匹配处理
完善入口文件中的 404 与 405 分支:
php
case FastRoute\Dispatcher::NOT_FOUND:
Response::error('资源不存在', 404);
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
Response::error('请求方法不允许', 405);
break;
6. 简单鉴权:API Key / Bearer Token
6.1 API Key 鉴权
API Key 适合服务端到服务端的调用,客户端在请求头中携带密钥:
php
<?php
// src/Auth/ApiKeyAuth.php
namespace App\Auth;
use App\Http\Request;
use App\Http\Response;
class ApiKeyAuth
{
private const VALID_KEYS = [
'live_key_abc123',
'test_key_xyz789',
];
public static function verify(): void
{
$key = Request::header('X-API-Key');
if (!$key || !in_array($key, self::VALID_KEYS, true)) {
Response::error('无效的 API Key', 401);
}
}
}
调用方在请求头中携带:
http
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: live_key_abc123
6.2 Bearer Token 鉴权
Bearer Token 更常用于用户态场景,客户端在 Authorization 头中携带令牌:
php
<?php
// src/Auth/BearerAuth.php
namespace App\Auth;
use App\Http\Request;
use App\Http\Response;
class BearerAuth
{
public static function verify(): void
{
$header = Request::header('Authorization');
if (!$header || !preg_match('/^Bearer\s+(\S+)$/', $header, $matches)) {
Response::error('缺少或格式错误的 Token', 401);
}
$token = $matches[1];
// 实际项目中应校验签名与过期时间,这里仅做示例
if (!self::isValid($token)) {
Response::error('Token 无效或已过期', 401);
}
}
private static function isValid(string $token): bool
{
// 可对接 Redis / 数据库 / JWT 验签
return $token === 'demo_token_2024';
}
}
请求示例:
http
GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer demo_token_2024
6.3 在控制器中组合使用
php
<?php
// src/Controller/UserController.php
namespace App\Controller;
use App\Auth\ApiKeyAuth;
use App\Auth\BearerAuth;
use App\Http\Response;
class UserController
{
public function index(array $params): void
{
ApiKeyAuth::verify(); // 服务端调用场景
// 业务逻辑...
Response::success([['id' => 1, 'name' => 'Alice']]);
}
public function show(array $params): void
{
BearerAuth::verify(); // 用户态场景
$id = (int) $params['id'];
// 业务逻辑...
Response::success(['id' => $id, 'name' => 'Alice']);
}
}
7. 完整请求流程串联
将以上模块在入口文件中串联起来,形成完整的请求生命周期:
php
<?php
// public/index.php 完整版
require __DIR__ . '/../vendor/autoload.php';
use App\Auth\ApiKeyAuth;
use App\Auth\BearerAuth;
use App\Http\Response;
use FastRoute\Dispatcher;
$dispatcher = FastRoute\simpleDispatcher(require __DIR__ . '/../routes.php');
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND:
Response::error('资源不存在', 404);
break;
case Dispatcher::METHOD_NOT_ALLOWED:
Response::error('请求方法不允许', 405);
break;
case Dispatcher::FOUND:
[$controller, $method] = explode('@', $routeInfo[1]);
$vars = $routeInfo[2];
$instance = new ("App\\Controller\\{$controller}")();
$instance->{$method}($vars);
break;
}
8. 总结
本文用 PHP 原生能力配合 FastRoute,搭建了一个轻量级 REST API 骨架,覆盖了从路由、请求解析、响应封装到鉴权的完整链路。核心要点如下:
- REST 设计:资源化 URL、语义化 HTTP 方法、无状态通信。
- 路由:FastRoute 提供高性能匹配与参数约束。
- 响应封装 :统一
{ code, message, data }结构,业务码与状态码解耦。 - 错误处理:全局异常捕获,生产环境隐藏堆栈。
- 鉴权:API Key 适合机器间调用,Bearer Token 适合用户态场景。
这套骨架足够支撑中小型项目的起步,后续可按需扩展中间件、参数校验、数据库访问层与日志系统。保持轻量、保持清晰,是构建可维护 API 的长久之道。