RESTful API,是现代Web开发中不可或缺的技术。无论是前后端分离、移动端开发、第三方服务对接,还是微服务架构,都离不开RESTful API。

作为一个PHP开发者,设计和实现RESTful API,是必备的技能。一个好的RESTful API,应该是简洁的、规范的、易用的、可扩展的、安全的。但在实际开发中,很多API设计得很糟糕,不规范,难用,难以维护。

今天,我们来详细学习PHP RESTful API的设计,从理论到实践,从REST的基本概念到API的设计原则,从路由设计到数据格式,从认证授权到错误处理,帮你掌握RESTful API的设计和实现。

什么是REST

REST,全称Representational State Transfer(表述性状态转移),是一种软件架构风格,由Roy Fielding在2000年的博士论文中提出。

REST不是一个标准,而是一种设计风格,一组约束和原则。符合REST约束和原则的架构,就称为RESTful架构。

REST的核心概念:

1. 资源(Resource)

REST中,一切皆资源。资源是网络上的一个实体,可以是一篇文章、一个用户、一张图片、一个订单等。

每个资源,都有一个唯一的URI(统一资源标识符)来标识。比如:

  • /articles/123:标识id为123的文章
  • /users/456:标识id为456的用户
  • /categories/789:标识id为789的分类

2. 表述(Representation)

资源的表述,是资源在某个时刻的状态的表示。资源可以有多种表述形式,如JSON、XML、HTML等。

客户端和服务器之间,传递的是资源的表述,而不是资源本身。客户端请求一个资源,服务器返回该资源的表述(如JSON格式的数据);客户端修改一个资源,发送该资源的表述给服务器。

3. 状态转移(State Transfer)

状态转移,是指通过HTTP方法,对资源进行操作,改变资源的状态。

REST使用HTTP方法来表示对资源的操作:

  • GET:获取资源(查询)
  • POST:创建资源
  • PUT:更新资源(全量更新)
  • PATCH:更新资源(部分更新)
  • DELETE:删除资源

通过这些HTTP方法,客户端可以对资源进行增删改查操作,实现资源状态的转移。

4. 无状态(Stateless)

REST是无状态的。服务器不保存客户端的状态,每个请求都是独立的,包含了处理该请求所需的所有信息。

无状态的好处:

  • 可扩展性好:服务器不需要维护会话,可以轻松扩展到多台服务器
  • 可靠性高:一个请求失败,不影响其他请求
  • 性能好:服务器不需要保存和恢复会话状态

5. 统一接口(Uniform Interface)

REST的统一接口,是指通过统一的HTTP方法和URI,对资源进行操作。不管是什么资源,都使用相同的HTTP方法(GET/POST/PUT/PATCH/DELETE)和相同的URI设计风格。

统一接口的好处:

  • 简单易用:开发者不需要学习不同的接口风格
  • 可预测性强:看到URI和HTTP方法,就知道要做什么
  • 解耦:客户端和服务器解耦,各自可以独立演化

REST的6个约束

Roy Fielding提出了REST的6个约束,符合这些约束的架构,才是真正的RESTful架构:

1. 客户端-服务器分离(Client-Server)

客户端和服务器分离,各自独立演化。客户端负责用户界面和用户交互,服务器负责数据存储和业务逻辑。

好处:关注点分离,提高了系统的可扩展性和可移植性。

2. 无状态(Stateless)

服务器不保存客户端的状态,每个请求都是独立的,包含了处理该请求所需的所有信息。

好处:可扩展性好,可靠性高,性能好。

3. 可缓存(Cacheable)

服务器的响应,可以被客户端缓存。缓存可以减少客户端和服务器之间的交互,提高性能和可扩展性。

服务器通过HTTP缓存头(如Cache-Control、ETag、Last-Modified)来控制缓存。

4. 统一接口(Uniform Interface)

通过统一的HTTP方法和URI,对资源进行操作。这是REST最核心的约束。

统一接口包含4个子约束:

  • 资源的标识(Resource identification in requests):通过URI标识资源
  • 通过表述操作资源(Resource manipulation through representations):通过资源的表述来操作资源
  • 自描述的消息(Self-descriptive messages):每个消息包含了足够的信息来描述如何处理它
  • 超媒体作为应用状态的引擎(Hypermedia as the engine of application state, HATEOAS):客户端通过服务器返回的超媒体链接,来发现和导航可用的操作

5. 分层系统(Layered System)

系统可以分层,客户端无法直接感知中间层(如代理、网关、负载均衡)。中间层可以提高系统的可扩展性、安全性和性能。

6. 按需代码(Code on Demand,可选)

服务器可以向客户端传输代码(如JavaScript),客户端执行这些代码,扩展客户端的功能。这是一个可选的约束。

RESTful API设计原则

设计一个好的RESTful API,应该遵循以下原则:

1. 使用名词,不使用动词

URI应该使用名词,表示资源,而不是使用动词,表示操作。

因为HTTP方法已经表示了操作(GET=查询、POST=创建、PUT=更新、DELETE=删除),URI不需要再包含动词。

好的设计

  • GET /articles:获取文章列表
  • GET /articles/123:获取id为123的文章
  • POST /articles:创建文章
  • PUT /articles/123:更新id为123的文章
  • DELETE /articles/123:删除id为123的文章

不好的设计

  • GET /getArticles:获取文章列表
  • GET /getArticle?id=123:获取id为123的文章
  • POST /createArticle:创建文章
  • POST /updateArticle?id=123:更新id为123的文章
  • POST /deleteArticle?id=123:删除id为123的文章

2. 使用复数名词

URI中的资源名,应该使用复数名词。

因为资源通常是一个集合(如文章集合、用户集合),使用复数更符合直觉。

好的设计

  • /articles:文章集合
  • /articles/123:文章集合中的id为123的文章
  • /users:用户集合
  • /users/456:用户集合中的id为456的用户

不好的设计

  • /article
  • /article/123
  • /user
  • /user/456

3. 层级关系

如果资源之间有层级关系(从属关系),应该在URI中体现出来。

比如,文章的评论,是从属于文章的;用户的订单,是从属于用户的。

好的设计

  • /articles/123/comments:文章123的评论列表
  • /articles/123/comments/456:文章123的id为456的评论
  • /users/456/orders:用户456的订单列表
  • /users/456/orders/789:用户456的id为789的订单

4. 使用HTTP方法表示操作

使用HTTP方法来表示对资源的操作,不要用URI或参数来表示操作。

HTTP方法操作幂等安全
GET获取资源
POST创建资源
PUT全量更新资源
PATCH部分更新资源
DELETE删除资源
  • 安全:不会改变服务器状态的方法(GET)
  • 幂等:执行多次和执行一次效果相同的方法(GET、PUT、DELETE)

5. 使用HTTP状态码

使用HTTP状态码来表示请求的结果,不要在响应体中用自定义的状态码。

常用的HTTP状态码:

2xx 成功

  • 200 OK:请求成功(GET、PUT、PATCH)
  • 201 Created:资源创建成功(POST)
  • 204 No Content:请求成功,但没有返回内容(DELETE)

3xx 重定向

  • 301 Moved Permanently:永久重定向
  • 302 Found:临时重定向
  • 304 Not Modified:资源未修改(缓存)

4xx 客户端错误

  • 400 Bad Request:请求参数错误
  • 401 Unauthorized:未认证
  • 403 Forbidden:已认证,但无权限
  • 404 Not Found:资源不存在
  • 405 Method Not Allowed:HTTP方法不允许
  • 409 Conflict:资源冲突(如重复创建)
  • 410 Gone:资源已永久删除
  • 415 Unsupported Media Type:不支持的媒体类型
  • 422 Unprocessable Entity:请求格式正确,但语义错误(如验证失败)
  • 429 Too Many Requests:请求过于频繁(限流)

5xx 服务器错误

  • 500 Internal Server Error:服务器内部错误
  • 502 Bad Gateway:网关错误
  • 503 Service Unavailable:服务不可用
  • 504 Gateway Timeout:网关超时

6. 统一的数据格式

API的请求和响应,应该使用统一的数据格式,推荐使用JSON。

JSON的优点:

  • 简洁,易读
  • 支持多种语言
  • 解析快
  • 是Web API的事实标准

响应格式应该统一,包含:

  • 数据(data):请求的数据
  • 元信息(meta):分页、总数等元信息
  • 错误信息(error):错误时的错误信息

成功响应示例

{
    "code": 0,
    "message": "success",
    "data": {
        "id": 123,
        "title": "文章标题",
        "content": "文章内容",
        "created_at": "2015-09-18 10:00:00"
    }
}

列表响应示例

{
    "code": 0,
    "message": "success",
    "data": [
        {"id": 1, "title": "文章1"},
        {"id": 2, "title": "文章2"}
    ],
    "meta": {
        "total": 100,
        "page": 1,
        "per_page": 10,
        "total_pages": 10
    }
}

错误响应示例

{
    "code": 40001,
    "message": "参数验证失败",
    "errors": {
        "title": ["标题不能为空", "标题长度不能超过200"],
        "content": ["内容不能为空"]
    }
}

7. 版本控制

API应该有版本控制,以便在API不兼容升级时,旧版本的客户端仍然可以使用。

常见的版本控制方式:

URI中包含版本(推荐)

  • /api/v1/articles
  • /api/v2/articles

优点:简单直观,易于路由 缺点:URI不那么"RESTful"

请求头中包含版本

  • Accept: application/vnd.myapi.v1+json
  • X-API-Version: 1

优点:URI干净 缺点:不直观,调试麻烦

查询参数中包含版本

  • /api/articles?version=1

不推荐,不优雅。

推荐使用URI中包含版本的方式,简单直观。

8. 认证和授权

API应该有认证和授权机制,确保只有合法的用户才能访问,且只能访问有权限的资源。

认证方式

  • API Key:在请求头或参数中传递API Key,简单但安全性一般
  • Basic Auth:用户名密码Base64编码,简单但不安全(需HTTPS)
  • Bearer Token(JWT):JSON Web Token,无状态,适合RESTful API
  • OAuth 2.0:第三方授权框架,适合开放平台

推荐使用JWT(JSON Web Token),无状态,适合RESTful API的无状态约束。

授权方式

  • 基于角色的访问控制(RBAC):用户有角色,角色有权限
  • 基于资源的访问控制:每个资源有所有者,只能访问自己的资源

9. 分页、排序、过滤

列表接口,应该支持分页、排序、过滤。

分页

  • /articles?page=1&per_page=10
  • 响应中包含分页元信息(total、page、perpage、totalpages)

排序

  • /articles?sort=created_at&order=desc
  • 支持多字段排序:/articles?sort=created_at,views&order=desc,asc

过滤

  • /articles?category_id=2&status=published
  • 支持范围过滤:/articles?createdatfrom=2015-01-01&createdatto=2015-12-31
  • 支持搜索:/articles?keyword=php

10. 错误处理

API应该有统一的错误处理,返回清晰的错误信息,帮助开发者定位问题。

错误响应应该包含:

  • 错误码(code):自定义的错误码,便于程序处理
  • 错误信息(message):人类可读的错误信息
  • 错误详情(errors):字段级别的错误详情(验证失败时)
  • 错误链接(documentation_url):可选,指向错误文档

11. 文档

API应该有完整的文档,帮助开发者快速上手。

文档应该包含:

  • API概述
  • 认证方式
  • 每个接口的详细说明(URI、HTTP方法、参数、响应、错误码)
  • 示例代码
  • 常见问题

常用的API文档工具:

  • Swagger/OpenAPI:标准的API文档规范,可自动生成文档和测试界面
  • Apiary:API设计和文档工具
  • Postman:API测试工具,也可以生成文档
  • 手写Markdown文档:简单灵活

12. HATEOAS(可选)

HATEOAS(Hypermedia as the Engine of Application State),是REST的最高境界。客户端不需要知道API的URI结构,只需要知道入口URI,然后通过服务器返回的超媒体链接,来发现和导航可用的操作。

示例

{
    "id": 123,
    "title": "文章标题",
    "content": "文章内容",
    "links": {
        "self": {"href": "/api/v1/articles/123", "method": "GET"},
        "update": {"href": "/api/v1/articles/123", "method": "PUT"},
        "delete": {"href": "/api/v1/articles/123", "method": "DELETE"},
        "comments": {"href": "/api/v1/articles/123/comments", "method": "GET"},
        "author": {"href": "/api/v1/users/456", "method": "GET"}
    }
}

HATEOAS的好处:

  • 客户端和服务器解耦,服务器可以自由修改URI结构
  • API自描述,客户端可以动态发现可用的操作
  • 减少客户端的硬编码

但HATEOAS实现复杂,大多数API都没有严格实现。可以根据需要,选择性地实现。

PHP实现RESTful API

下面,以我们的博客系统为例,演示如何用PHP实现RESTful API。

1. 项目结构

blog/
├── api/
│   ├── index.php          # API入口
│   ├── config.php         # 配置文件
│   ├── Database.php       # 数据库类
│   ├── Router.php         # 路由类
│   ├── JWT.php            # JWT类
│   ├── Response.php       # 响应类
│   ├── controllers/
│   │   ├── ArticleController.php
│   │   ├── CategoryController.php
│   │   ├── UserController.php
│   │   └── CommentController.php
│   └── models/
│       ├── Article.php
│       ├── Category.php
│       ├── User.php
│       └── Comment.php
└── ...

2. 入口文件(api/index.php)

<?php
// API入口文件
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, Authorization');

// 处理OPTIONS预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/Router.php';
require_once __DIR__ . '/Response.php';

// 路由
$router = new Router();

// 文章路由
$router->get('/api/v1/articles', 'ArticleController@index');
$router->get('/api/v1/articles/{id}', 'ArticleController@show');
$router->post('/api/v1/articles', 'ArticleController@store');
$router->put('/api/v1/articles/{id}', 'ArticleController@update');
$router->delete('/api/v1/articles/{id}', 'ArticleController@destroy');

// 分类路由
$router->get('/api/v1/categories', 'CategoryController@index');
$router->get('/api/v1/categories/{id}', 'CategoryController@show');

// 用户路由
$router->post('/api/v1/auth/login', 'UserController@login');
$router->post('/api/v1/auth/register', 'UserController@register');
$router->get('/api/v1/user', 'UserController@profile');

// 评论路由
$router->get('/api/v1/articles/{articleId}/comments', 'CommentController@index');
$router->post('/api/v1/articles/{articleId}/comments', 'CommentController@store');
$router->delete('/api/v1/comments/{id}', 'CommentController@destroy');

// 执行路由
try {
    $router->dispatch();
} catch (Exception $e) {
    Response::error($e->getCode() ?: 500, $e->getMessage());
}

3. 路由类(Router.php)

<?php
class Router {
    private $routes = [];

    public function get($pattern, $handler) {
        $this->addRoute('GET', $pattern, $handler);
    }

    public function post($pattern, $handler) {
        $this->addRoute('POST', $pattern, $handler);
    }

    public function put($pattern, $handler) {
        $this->addRoute('PUT', $pattern, $handler);
    }

    public function patch($pattern, $handler) {
        $this->addRoute('PATCH', $pattern, $handler);
    }

    public function delete($pattern, $handler) {
        $this->addRoute('DELETE', $pattern, $handler);
    }

    private function addRoute($method, $pattern, $handler) {
        // 将{param}转换为正则
        $pattern = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', '([^/]+)', $pattern);
        $regex = '#^' . $pattern . '$#';
        $this->routes[] = [
            'method' => $method,
            'regex' => $regex,
            'handler' => $handler
        ];
    }

    public function dispatch() {
        $method = $_SERVER['REQUEST_METHOD'];
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

        foreach ($this->routes as $route) {
            if ($route['method'] !== $method) continue;
            if (preg_match($route['regex'], $uri, $matches)) {
                // 提取参数
                array_shift($matches);
                $params = $matches;

                // 解析handler
                list($controllerName, $action) = explode('@', $route['handler']);
                $controllerClass = $controllerName;
                $controllerFile = __DIR__ . '/controllers/' . $controllerName . '.php';

                if (!file_exists($controllerFile)) {
                    throw new Exception("Controller not found: $controllerName", 404);
                }

                require_once $controllerFile;
                $controller = new $controllerClass();

                if (!method_exists($controller, $action)) {
                    throw new Exception("Action not found: $action", 404);
                }

                // 调用方法
                call_user_func_array([$controller, $action], $params);
                return;
            }
        }

        throw new Exception('Route not found', 404);
    }
}

4. 响应类(Response.php)

<?php
class Response {
    public static function json($data, $code = 200) {
        http_response_code($code);
        echo json_encode([
            'code' => 0,
            'message' => 'success',
            'data' => $data
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public static function success($data = null, $message = 'success') {
        echo json_encode([
            'code' => 0,
            'message' => $message,
            'data' => $data
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public static function error($code, $message, $errors = null, $httpCode = 400) {
        http_response_code($httpCode);
        $response = [
            'code' => $code,
            'message' => $message
        ];
        if ($errors !== null) {
            $response['errors'] = $errors;
        }
        echo json_encode($response, JSON_UNESCAPED_UNICODE);
        exit;
    }

    public static function paginate($items, $total, $page, $perPage) {
        echo json_encode([
            'code' => 0,
            'message' => 'success',
            'data' => $items,
            'meta' => [
                'total' => $total,
                'page' => $page,
                'per_page' => $perPage,
                'total_pages' => ceil($total / $perPage)
            ]
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public static function created($data) {
        http_response_code(201);
        echo json_encode([
            'code' => 0,
            'message' => 'created',
            'data' => $data
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public static function noContent() {
        http_response_code(204);
        exit;
    }
}

5. 文章控制器(ArticleController.php)

<?php
require_once __DIR__ . '/../models/Article.php';
require_once __DIR__ . '/../Response.php';

class ArticleController {
    private $articleModel;

    public function __construct() {
        $this->articleModel = new Article();
    }

    // GET /api/v1/articles
    public function index() {
        $page = max(1, intval($_GET['page'] ?? 1));
        $perPage = min(100, max(1, intval($_GET['per_page'] ?? 10)));
        $categoryId = intval($_GET['category_id'] ?? 0);
        $keyword = trim($_GET['keyword'] ?? '');
        $sort = $_GET['sort'] ?? 'created_at';
        $order = strtoupper($_GET['order'] ?? 'DESC');

        $allowedSorts = ['created_at', 'views', 'id'];
        if (!in_array($sort, $allowedSorts)) $sort = 'created_at';
        if (!in_array($order, ['ASC', 'DESC'])) $order = 'DESC';

        $result = $this->articleModel->getList($page, $perPage, $categoryId, $keyword, $sort, $order);
        Response::paginate($result['items'], $result['total'], $page, $perPage);
    }

    // GET /api/v1/articles/{id}
    public function show($id) {
        $article = $this->articleModel->getById($id);
        if (!$article) {
            Response::error(40401, '文章不存在', null, 404);
        }
        // 增加阅读量
        $this->articleModel->incrementViews($id);
        Response::success($article);
    }

    // POST /api/v1/articles
    public function store() {
        // 认证检查
        $this->requireAuth();

        $input = json_decode(file_get_contents('php://input'), true);

        // 验证
        $errors = [];
        if (empty($input['title'])) $errors['title'][] = '标题不能为空';
        if (mb_strlen($input['title'] ?? '') > 200) $errors['title'][] = '标题长度不能超过200';
        if (empty($input['content'])) $errors['content'][] = '内容不能为空';
        if (empty($input['category_id'])) $errors['category_id'][] = '分类不能为空';

        if (!empty($errors)) {
            Response::error(40001, '参数验证失败', $errors, 422);
        }

        $article = $this->articleModel->create($input);
        Response::created($article);
    }

    // PUT /api/v1/articles/{id}
    public function update($id) {
        $this->requireAuth();

        $article = $this->articleModel->getById($id);
        if (!$article) {
            Response::error(40401, '文章不存在', null, 404);
        }

        $input = json_decode(file_get_contents('php://input'), true);
        $updated = $this->articleModel->update($id, $input);
        Response::success($updated);
    }

    // DELETE /api/v1/articles/{id}
    public function destroy($id) {
        $this->requireAuth();

        $article = $this->articleModel->getById($id);
        if (!$article) {
            Response::error(40401, '文章不存在', null, 404);
        }

        $this->articleModel->delete($id);
        Response::noContent();
    }

    private function requireAuth() {
        $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
        if (!preg_match('/Bearer\s+(.*)/', $authHeader, $matches)) {
            Response::error(40101, '未认证', null, 401);
        }
        $token = $matches[1];
        // 验证JWT token
        // ...
    }
}

6. 文章模型(Article.php)

<?php
require_once __DIR__ . '/../Database.php';

class Article {
    private $db;

    public function __construct() {
        $this->db = Database::getInstance();
    }

    public function getList($page, $perPage, $categoryId = 0, $keyword = '', $sort = 'created_at', $order = 'DESC') {
        $offset = ($page - 1) * $perPage;
        $where = "WHERE status = 'published'";
        $params = [];

        if ($categoryId > 0) {
            $where .= " AND category_id = ?";
            $params[] = $categoryId;
        }

        if ($keyword) {
            $where .= " AND (title LIKE ? OR content LIKE ?)";
            $params[] = "%$keyword%";
            $params[] = "%$keyword%";
        }

        // 总数
        $countSql = "SELECT COUNT(*) FROM blog_posts $where";
        $stmt = $this->db->prepare($countSql);
        $stmt->execute($params);
        $total = $stmt->fetchColumn();

        // 列表
        $sql = "SELECT id, title, slug, excerpt, cover, category_id, views, created_at 
                FROM blog_posts 
                $where 
                ORDER BY $sort $order 
                LIMIT ? OFFSET ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute(array_merge($params, [$perPage, $offset]));
        $items = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return ['items' => $items, 'total' => $total];
    }

    public function getById($id) {
        $sql = "SELECT * FROM blog_posts WHERE id = ? AND status = 'published'";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function create($data) {
        $slug = $data['slug'] ?? $this->generateSlug($data['title']);
        $excerpt = $data['excerpt'] ?? mb_substr(strip_tags($data['content']), 0, 200);
        $now = date('Y-m-d H:i:s');

        $sql = "INSERT INTO blog_posts (title, slug, content, excerpt, category_id, cover, tags, status, views, created_at, updated_at) 
                VALUES (?, ?, ?, ?, ?, ?, ?, 'published', 0, ?, ?)";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            $data['title'], $slug, $data['content'], $excerpt,
            $data['category_id'], $data['cover'] ?? '', $data['tags'] ?? '',
            $now, $now
        ]);

        $id = $this->db->lastInsertId();
        return $this->getById($id);
    }

    public function update($id, $data) {
        $fields = [];
        $params = [];
        $allowed = ['title', 'content', 'excerpt', 'category_id', 'cover', 'tags', 'status'];

        foreach ($allowed as $field) {
            if (isset($data[$field])) {
                $fields[] = "$field = ?";
                $params[] = $data[$field];
            }
        }

        if (empty($fields)) return $this->getById($id);

        $fields[] = "updated_at = ?";
        $params[] = date('Y-m-d H:i:s');
        $params[] = $id;

        $sql = "UPDATE blog_posts SET " . implode(', ', $fields) . " WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        return $this->getById($id);
    }

    public function delete($id) {
        $sql = "UPDATE blog_posts SET status = 'trash' WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        return $stmt->execute([$id]);
    }

    public function incrementViews($id) {
        $sql = "UPDATE blog_posts SET views = views + 1 WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        return $stmt->execute([$id]);
    }

    private function generateSlug($title) {
        // 简单的slug生成,实际可以用拼音转换
        return 'post-' . time();
    }
}

7. Nginx配置

server {
    listen 80;
    server_name api.example.com;
    root /var/www/blog/api;
    index index.php;

    # URL重写,所有请求都走index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP处理
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # 缓存静态文件
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    # 限流
    limit_req zone=api burst=20 nodelay;
}

API安全

1. HTTPS

API必须使用HTTPS,确保数据传输的安全性。不要在HTTP上传输敏感信息(如密码、Token)。

2. 认证

使用JWT或OAuth 2.0进行认证,不要使用Cookie/Session(REST是无状态的)。

3. 授权

实现基于角色或资源的授权,确保用户只能访问有权限的资源。

4. 输入验证

对所有输入进行验证,防止SQL注入、XSS、CSRF等攻击。

  • 使用PDO预处理语句,防止SQL注入
  • 对输出进行HTML转义,防止XSS
  • 使用Token验证,防止CSRF(API通常用JWT,不需要CSRF)

5. 限流

实现API限流,防止滥用和DDoS攻击。可以用Nginx的limit_req,或应用层的限流(如Redis实现的令牌桶算法)。

6. 敏感信息

不要在响应中返回敏感信息(如密码、密码哈希、内部ID等)。

7. 错误信息

不要在错误信息中暴露服务器内部信息(如文件路径、数据库错误详情等),防止信息泄露。

API性能优化

1. 缓存

对不常变化的数据进行缓存,如文章列表、分类列表等。可以用Redis缓存,减少数据库查询。

2. 分页

列表接口必须分页,避免一次返回太多数据,影响性能。

3. 字段选择

支持字段选择,客户端可以只请求需要的字段,减少数据传输量。

  • /articles?fields=id,title,created_at

4. 关联数据

避免N+1查询,使用JOIN或预加载,减少数据库查询次数。

5. 压缩

启用Gzip压缩,减少响应数据的大小。

6. 数据库优化

合理使用索引,优化SQL查询,避免慢查询。

总结

RESTful API设计,是现代Web开发的重要技能。

核心要点:

  1. 什么是REST:表述性状态转移,一种软件架构风格,核心概念是资源、表述、状态转移、无状态、统一接口
  2. REST的6个约束:客户端-服务器分离、无状态、可缓存、统一接口、分层系统、按需代码(可选)
  3. 设计原则:使用名词不使用动词、使用复数名词、层级关系、使用HTTP方法表示操作、使用HTTP状态码、统一数据格式(JSON)、版本控制、认证授权、分页排序过滤、统一错误处理、文档、HATEOAS(可选)
  4. PHP实现:入口文件、路由类、响应类、控制器、模型、Nginx配置
  5. API安全:HTTPS、认证(JWT)、授权、输入验证、限流、敏感信息保护、错误信息保护
  6. 性能优化:缓存、分页、字段选择、关联数据优化、压缩、数据库优化

一个好的RESTful API,应该是简洁的、规范的、易用的、可扩展的、安全的、高性能的。设计API时,要站在使用者的角度,考虑易用性和可维护性,遵循REST的原则和最佳实践。

"好的API,是好的产品的一半。"希望这篇文章,能帮你掌握RESTful API的设计和实现,设计出简洁、规范、易用的API。