支持 RESTful

为了增加对 RESTful 的支持,需要确保框架能够处理基于 REST(Representational State Transfer)架构风格的请求。这包括正确使用 HTTP 方法、URI 设计、状态码响应以及数据格式(如 JSON)的处理。以下将详细介绍如何在现有框架中集成 RESTful 支持,包括关键步骤和代码示例。

目录

  1. 理解 RESTful 架构

  2. 定义 RESTful 路由

  3. 创建 RESTful 控制器

  4. 更新 Router 以支持 RESTful 请求

  5. 处理 JSON 请求和响应

  6. 状态码和错误处理

  7. 示例:实现用户的 RESTful API

  8. 总结

  9. 进一步扩展建议


理解 RESTful 架构

RESTful 架构 是一种设计风格,旨在构建可扩展和可维护的网络应用。其核心理念包括:

  • 资源(Resources):应用中的实体,如用户、文章等,每个资源通过唯一的 URI 进行标识。

  • HTTP 方法:使用标准的 HTTP 方法(GET、POST、PUT、DELETE)来操作资源。

  • 无状态(Stateless):每个请求都应包含所有必要的信息,服务器不存储客户端的会话信息。

  • 数据格式:通常使用 JSON 作为数据交换格式,易于解析和处理。


定义 RESTful 路由

首先,需要在路由器中定义 RESTful 路由。这些路由通常对应于资源的 CRUD 操作:

  • GET /resources:获取资源列表(Index)

  • GET /resources/{id}:获取单个资源详情(Show)

  • POST /resources:创建新资源(Store)

  • PUT/PATCH /resources/{id}:更新资源(Update)

  • DELETE /resources/{id}:删除资源(Destroy)

示例:定义用户资源的 RESTful 路由

index.php 或路由配置文件中添加以下路由:

<?php
// index.php

use MyFramework\Router;
use MyFramework\Controllers\UsersController;
use MyFramework\Middleware\AuthenticationMiddleware;

// ... 之前的代码 ...

// 定义 RESTful 路由
$router->add('GET', '/users', 'UsersController@index', [AuthenticationMiddleware::class], 'users.index');
$router->add('GET', '/users/{id}', 'UsersController@show', [AuthenticationMiddleware::class], 'users.show');
$router->add('POST', '/users', 'UsersController@store', [AuthenticationMiddleware::class], 'users.store');
$router->add('PUT', '/users/{id}', 'UsersController@update', [AuthenticationMiddleware::class], 'users.update');
$router->add('DELETE', '/users/{id}', 'UsersController@destroy', [AuthenticationMiddleware::class], 'users.destroy');

// 处理请求
$router->dispatch($requestMethod, $requestUri);
?>

说明

  • 每个路由对应一个 HTTP 方法和 URI 模式。

  • 使用命名路由(如 users.index)便于反向路由生成。

  • 大部分操作需要认证中间件保护,确保只有认证用户可以执行。


创建 RESTful 控制器

接下来,需要创建一个控制器来处理这些 RESTful 路由对应的请求。控制器应包含以下方法:

  • index:获取资源列表

  • show:获取单个资源详情

  • store:创建新资源

  • update:更新资源

  • destroy:删除资源

示例:创建 UsersController

src/Controllers/ 目录下创建 UsersController.php 文件:

<?php
// src/Controllers/UsersController.php

namespace MyFramework\Controllers;

use MyFramework\Controller;
use Monolog\Logger;

class UsersController extends Controller
{
    private $logger;

    public function __construct(\Twig\Environment $twig, Logger $logger, \MyFramework\Router $router)
    {
        parent::__construct($twig, $logger, $router);
        $this->logger = $logger;
    }

    /**
     * 获取用户列表
     */
    public function index()
    {
        $this->logger->info("获取用户列表");

        // 示例数据,实际应用中应从数据库获取
        $users = [
            ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
            ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'],
            ['id' => 3, 'name' => '王五', 'email' => 'wangwu@example.com'],
        ];

        $this->jsonResponse($users);
    }

    /**
     * 获取单个用户详情
     *
     * @param int $id 用户ID
     */
    public function show($id)
    {
        $this->logger->info("获取用户详情", ['id' => $id]);

        // 示例数据,实际应用中应从数据库获取
        $users = [
            1 => ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
            2 => ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'],
            3 => ['id' => 3, 'name' => '王五', 'email' => 'wangwu@example.com'],
        ];

        if (isset($users[$id])) {
            $this->jsonResponse($users[$id]);
        } else {
            $this->jsonResponse(['error' => '用户未找到'], 404);
        }
    }

    /**
     * 创建新用户
     */
    public function store()
    {
        $this->logger->info("创建新用户");

        // 获取 JSON 请求体
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input || !isset($input['name']) || !isset($input['email'])) {
            $this->jsonResponse(['error' => '无效的输入'], 400);
            return;
        }

        // 示例:创建用户逻辑,实际应用中应插入数据库
        $newUser = [
            'id' => rand(100, 999), // 示例ID
            'name' => htmlspecialchars($input['name']),
            'email' => htmlspecialchars($input['email']),
        ];

        $this->jsonResponse($newUser, 201);
    }

    /**
     * 更新用户信息
     *
     * @param int $id 用户ID
     */
    public function update($id)
    {
        $this->logger->info("更新用户信息", ['id' => $id]);

        // 获取 JSON 请求体
        $input = json_decode(file_get_contents('php://input'), true);

        if (!$input) {
            $this->jsonResponse(['error' => '无效的输入'], 400);
            return;
        }

        // 示例数据,实际应用中应从数据库获取并更新
        $users = [
            1 => ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
            2 => ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'],
            3 => ['id' => 3, 'name' => '王五', 'email' => 'wangwu@example.com'],
        ];

        if (isset($users[$id])) {
            $users[$id] = array_merge($users[$id], array_map('htmlspecialchars', $input));
            $this->jsonResponse($users[$id]);
        } else {
            $this->jsonResponse(['error' => '用户未找到'], 404);
        }
    }

    /**
     * 删除用户
     *
     * @param int $id 用户ID
     */
    public function destroy($id)
    {
        $this->logger->info("删除用户", ['id' => $id]);

        // 示例数据,实际应用中应从数据库删除
        $users = [
            1 => ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
            2 => ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'],
            3 => ['id' => 3, 'name' => '王五', 'email' => 'wangwu@example.com'],
        ];

        if (isset($users[$id])) {
            unset($users[$id]);
            $this->jsonResponse(['message' => '用户已删除']);
        } else {
            $this->jsonResponse(['error' => '用户未找到'], 404);
        }
    }

    /**
     * 发送 JSON 响应
     *
     * @param mixed $data 数据
     * @param int $status HTTP 状态码
     */
    protected function jsonResponse($data, $status = 200)
    {
        header_remove();
        http_response_code($status);
        header("Content-Type: application/json");
        echo json_encode($data);
        exit();
    }
}
?>

说明

  • 方法定义

    • index:返回用户列表。

    • show:返回指定 ID 的用户详情。

    • store:创建新用户,接收 JSON 格式的请求体。

    • update:更新指定 ID 的用户信息,接收 JSON 格式的请求体。

    • destroy:删除指定 ID 的用户。

  • JSON 处理

    • 使用 json_decode 解析请求体中的 JSON 数据。

    • 使用 json_encode 发送 JSON 响应,并设置正确的 Content-Type 头。

  • 状态码

    • 正常操作返回 200

    • 创建操作返回 201

    • 输入无效返回 400

    • 资源未找到返回 404


更新 Router 以支持 RESTful 请求

确保路由器能够正确处理不同的 HTTP 方法,并将请求路由到相应的控制器方法。

关键修改点

  1. 支持多种 HTTP 方法

    • 确保路由器能够根据请求的 HTTP 方法(GET、POST、PUT、DELETE)进行路由匹配。

  2. 集成 RESTful 路由

    • 根据定义的 RESTful 路由,将请求转发到相应的控制器方法。

示例:更新 Router 类

以下是关键部分的示例:

<?php
// src/Router.php

namespace MyFramework;

use MyFramework\Middleware\Middleware;
use ReflectionException;

class Router
{
    private $routes = [];
    private $namedRoutes = [];
    private $container;

    /**
     * 构造函数
     *
     * @param Container $container 依赖注入容器
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->loadCache(); // 如果实现了路由缓存
    }

    /**
     * 添加路由规则
     *
     * @param string $method HTTP 方法
     * @param string $uri 请求的 URI
     * @param string $action 控制器和方法,例如 'UsersController@show'
     * @param array $middlewares 中间件列表
     * @param string|null $name 路由名称
     */
    public function add(string $method, string $uri, string $action, array $middlewares = [], string $name = null)
    {
        // 转换 URI 模式为正则表达式,并提取参数名称
        $pattern = preg_replace_callback('/\{([a-zA-Z0-9_]+)(\?)?\}/', function ($matches) {
            $param = $matches[1];
            $optional = isset($matches[2]) && $matches[2] === '?';
            if ($optional) {
                return '(?P<' . $param . '>[a-zA-Z0-9_-]+)?';
            } else {
                return '(?P<' . $param . '>[a-zA-Z0-9_-]+)';
            }
        }, $uri);

        // 支持可选参数后的斜杠
        $pattern = preg_replace('#//+#', '/', $pattern);
        $pattern = '#^' . $pattern . '(/)?$#';

        // 提取参数名称
        $params = $this->extractParams($uri);

        $this->routes[] = [
            'method'      => strtoupper($method),
            'pattern'     => $pattern,
            'action'      => $action,
            'params'      => $params,
            'middlewares' => $middlewares,
            'name'        => $name
        ];

        if ($name) {
            $this->namedRoutes[$name] = end($this->routes);
        }
    }

    /**
     * 分发请求到相应的控制器方法
     *
     * @param string $requestMethod HTTP 方法
     * @param string $requestUri 请求的 URI
     */
    public function dispatch(string $requestMethod, string $requestUri)
    {
        foreach ($this->routes as $route) {
            if ($route['method'] === strtoupper($requestMethod)) {
                if (preg_match($route['pattern'], $requestUri, $matches)) {
                    // 提取命名参数
                    $params = [];
                    foreach ($route['params'] as $param) {
                        if (isset($matches[$param]) && $matches[$param] !== '') {
                            $params[$param] = $matches[$param];
                        }
                    }

                    // 执行中间件
                    foreach ($route['middlewares'] as $middlewareClass) {
                        /** @var Middleware $middleware */
                        $middleware = $this->container->make($middlewareClass);
                        if (!$middleware->handle($params)) {
                            // 中间件中止请求
                            return;
                        }
                    }

                    // 解析控制器和方法
                    list($controllerName, $method) = explode('@', $route['action']);
                    $fullControllerName = "MyFramework\\Controllers\\$controllerName";

                    // 通过容器获取控制器实例
                    if ($this->container->make($fullControllerName)) {
                        $controller = $this->container->make($fullControllerName);
                        if (method_exists($controller, $method)) {
                            // 调用方法并传递参数
                            call_user_func_array([$controller, $method], $params);
                            return;
                        }
                    }

                    // 如果控制器或方法不存在,返回 404
                    $this->sendNotFound();
                }
            }
        }
        // 如果没有匹配的路由,返回 404
        $this->sendNotFound();
    }

    /**
     * 发送 404 响应
     */
    private function sendNotFound()
    {
        header("HTTP/1.0 404 Not Found");
        echo json_encode(['error' => '资源未找到']);
        exit();
    }

    /**
     * 提取路由中的参数名称
     *
     * @param string $uri 路由 URI
     * @return array 参数名称列表
     */
    private function extractParams(string $uri): array
    {
        preg_match_all('/\{([a-zA-Z0-9_]+)(\?)?\}/', $uri, $matches);
        return $matches[1];
    }

    // ... 路由缓存相关方法 ...
}
?>

说明

  • HTTP 方法支持:通过路由定义中的 method 参数,路由器能够区分不同的 HTTP 方法(GET、POST、PUT、DELETE)。

  • JSON 404 响应:在 sendNotFound 方法中,返回 JSON 格式的错误信息,符合 RESTful API 的最佳实践。

  • 中间件执行:在分发请求前执行中间件,确保请求的合法性和安全性。


处理 JSON 请求和响应

RESTful API 通常使用 JSON 作为数据交换格式。因此,框架需要能够:

  1. 解析 JSON 请求体:将客户端发送的 JSON 数据解析为 PHP 数组或对象。

  2. 生成 JSON 响应:将 PHP 数据结构编码为 JSON,并设置正确的 Content-Type 头。

示例:在控制器中处理 JSON

UsersController 为例,已在前面的代码中展示了如何处理 JSON 请求和响应。以下是关键部分的总结:

<?php
/**
 * 创建新用户
 */
public function store()
{
    $this->logger->info("创建新用户");

    // 获取 JSON 请求体
    $input = json_decode(file_get_contents('php://input'), true);

    if (!$input || !isset($input['name']) || !isset($input['email'])) {
        $this->jsonResponse(['error' => '无效的输入'], 400);
        return;
    }

    // 创建用户逻辑...

    $this->jsonResponse($newUser, 201);
}

/**
 * 发送 JSON 响应
 *
 * @param mixed $data 数据
 * @param int $status HTTP 状态码
 */
protected function jsonResponse($data, $status = 200)
{
    header_remove();
    http_response_code($status);
    header("Content-Type: application/json");
    echo json_encode($data);
    exit();
}

说明

  • 解析请求:使用 file_get_contents('php://input') 获取原始请求体,并通过 json_decode 解析为 PHP 数组。

  • 生成响应:通过 json_encode 将 PHP 数据结构转换为 JSON,并设置 Content-Typeapplication/json

  • 状态码:根据操作结果设置合适的 HTTP 状态码,如 200201400404 等。


状态码和错误处理

正确使用 HTTP 状态码有助于客户端理解请求的结果。以下是一些常用的状态码及其用途:

  • 200 OK:请求成功,适用于 GET、PUT 等请求。

  • 201 Created:成功创建资源,适用于 POST 请求。

  • 400 Bad Request:客户端请求有误,适用于输入验证失败。

  • 401 Unauthorized:未认证,适用于需要认证的请求。

  • 403 Forbidden:禁止访问,适用于权限不足。

  • 404 Not Found:资源未找到,适用于请求的资源不存在。

  • 500 Internal Server Error:服务器内部错误,适用于未捕获的异常。

示例:在控制器中处理错误

<?php
public function show($id)
{
    $this->logger->info("获取用户详情", ['id' => $id]);

    // 示例数据,实际应用中应从数据库获取
    $users = [
        1 => ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
        2 => ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'],
        3 => ['id' => 3, 'name' => '王五', 'email' => 'wangwu@example.com'],
    ];

    if (isset($users[$id])) {
        $this->jsonResponse($users[$id]);
    } else {
        $this->jsonResponse(['error' => '用户未找到'], 404);
    }
}

说明

  • 成功响应:用户存在时,返回 200 OK 和用户数据。

  • 错误响应:用户不存在时,返回 404 Not Found 和错误信息。


示例:实现用户的 RESTful API

以下是一个综合示例,展示如何实现用户资源的 RESTful API,包括路由定义、控制器实现以及 JSON 处理。

1. 路由定义

index.php 中定义用户的 RESTful 路由:

<?php
// index.php

use MyFramework\Router;
use MyFramework\Controllers\UsersController;
use MyFramework\Middleware\AuthenticationMiddleware;

// ... 之前的代码 ...

// 定义 RESTful 用户路由
$router->add('GET', '/users', 'UsersController@index', [AuthenticationMiddleware::class], 'users.index');
$router->add('GET', '/users/{id}', 'UsersController@show', [AuthenticationMiddleware::class], 'users.show');
$router->add('POST', '/users', 'UsersController@store', [AuthenticationMiddleware::class], 'users.store');
$router->add('PUT', '/users/{id}', 'UsersController@update', [AuthenticationMiddleware::class], 'users.update');
$router->add('DELETE', '/users/{id}', 'UsersController@destroy', [AuthenticationMiddleware::class], 'users.destroy');

// 处理请求
$router->dispatch($requestMethod, $requestUri);
?>

2. 控制器实现

UsersController 已在前述部分详细介绍。关键点包括:

  • 使用 jsonResponse 方法返回 JSON 格式的数据。

  • 处理不同的 HTTP 方法对应的 CRUD 操作。

  • 适当使用 HTTP 状态码。

3. 测试 RESTful API

可以使用工具如 PostmancURL 来测试 RESTful API。

a. 获取用户列表

curl -X GET http://localhost:8000/users -H "Authorization: Bearer YOUR_TOKEN"

响应

[
    {"id":1,"name":"张三","email":"zhangsan@example.com"},
    {"id":2,"name":"李四","email":"lisi@example.com"},
    {"id":3,"name":"王五","email":"wangwu@example.com"}
]

b. 获取单个用户

curl -X GET http://localhost:8000/users/1 -H "Authorization: Bearer YOUR_TOKEN"

响应

{"id":1,"name":"张三","email":"zhangsan@example.com"}

c. 创建新用户

curl -X POST http://localhost:8000/users \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -d '{"name":"赵六","email":"zhaoliu@example.com"}'

响应

{"id":123,"name":"赵六","email":"zhaoliu@example.com"}

d. 更新用户

curl -X PUT http://localhost:8000/users/1 \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -d '{"email":"newemail@example.com"}'

响应

{"id":1,"name":"张三","email":"newemail@example.com"}

e. 删除用户

curl -X DELETE http://localhost:8000/users/1 -H "Authorization: Bearer YOUR_TOKEN"

响应

{"message":"用户已删除"}

说明

  • 认证:假设 AuthenticationMiddleware 已实现,使用合适的认证方式(如 Bearer Token)进行请求认证。

  • 数据格式:所有请求和响应均使用 JSON 格式,符合 RESTful API 的最佳实践。


总结

通过以上步骤,您已经在自定义的 PHP 框架中成功集成了 RESTful 支持,实现了以下功能:

  1. RESTful 路由定义

    • 根据资源的 CRUD 操作,使用合适的 HTTP 方法和 URI 设计路由。

  2. RESTful 控制器实现

    • 创建控制器方法(index、show、store、update、destroy)来处理不同的请求。

  3. JSON 处理

    • 解析 JSON 请求体,生成 JSON 响应,确保数据交换的标准化。

  4. 状态码和错误处理

    • 根据请求结果返回合适的 HTTP 状态码和错误信息,提高 API 的可用性和可理解性。

  5. 中间件保护

    • 使用认证中间件保护敏感的 RESTful 路由,确保只有授权用户可以访问。

优势

  • 一致性:遵循 RESTful 原则,使 API 更加一致和易于理解。

  • 可维护性:通过标准化的路由和控制器方法,提升代码的可维护性和可扩展性。

  • 安全性:结合中间件,实现请求的认证和授权,保障 API 的安全性。

  • 灵活性:使用 JSON 作为数据格式,便于与各种客户端进行数据交换。


进一步扩展建议

为了进一步提升 RESTful API 的功能和用户体验,还可以考虑以下扩展:

  1. 版本控制

    • 在 URI 中引入版本号,如 /api/v1/users,以支持 API 的迭代和向后兼容。

  2. 分页

    • 对于返回大量资源的 index 方法,实现分页功能,优化性能和响应时间。

  3. 过滤和排序

    • 允许客户端通过查询参数进行资源的过滤和排序,提高 API 的灵活性。

  4. 认证和授权

    • 实现更复杂的认证机制,如 OAuth2,确保 API 的安全性。

    • 基于角色的访问控制,限制不同用户的访问权限。

  5. API 文档

    • 使用工具如 SwaggerApiDoc 自动生成 API 文档,提升开发效率和 API 的可用性。

  6. 错误标准化

    • 定义统一的错误响应格式,提供详细的错误信息和解决建议。

  7. 中间件扩展

    • 添加更多的中间件,如日志记录、请求限流、CORS 处理等,提升 API 的功能和性能。

  8. 测试

    • 编写单元测试和集成测试,确保 API 的稳定性和可靠性。

  9. 缓存

    • 对于高频访问的资源,实现响应缓存,提升性能和用户体验。