PHP 专家级设计思考:论如何通过中间件(Middleware)模式实现跨多框架的业务逻辑逻辑标准化

各位下午好,欢迎来到这场名为“别让你的业务逻辑变成意大利面”的讲座。

坐在台下的各位,我想问个直击灵魂的问题:你们写过多少个“类似项目”?

是不是那种情况:老板说“我们要做一个电商后台”,你用 Laravel 搞定了;半年后,老板说“我们要做一个客户管理系统”,你顺手又拿出了 Laravel;再过半年,老板突发奇想,我们要做一个内部 API 网关,结果你发现,虽然底层的 CRUD 逻辑差不多,但你不得不重新造轮子,因为这次客户坚持要用 Slim,而团队里还有个 CodeIgniter 的死忠粉。

这就是我们面临的“框架围城”。每个框架都有自己的一套中间件机制,一套认证逻辑,一套日志系统。你为了追求开发速度,把 Auth::check() 写在 Laravel 里,把 use Auth; 写在 Slim 里。结果呢?当你想把这些代码复用到另一个项目时,发现它们像是一坨粘连在一起的生面团,怎么扯都扯不开。

今天,我们就来聊聊如何通过中间件(Middleware)模式,打破这些框架的围墙,建立一套跨框架的业务逻辑标准化体系。

我们不聊虚的,直接上干货。在此之前,请想象一下你是一家连锁餐厅的后厨经理。

第一章:厨房里的“服务员”——什么是中间件?

在 PHP 的世界里,中间件是什么?很多人说它是洋葱。洋葱有两层意思:一层是核心逻辑,一层是包在周围的层层包裹。每次请求进来,都要经过一系列的“安检”和“清洗”,最后才能见到核心的“厨师”(你的 Controller)。

如果把你写的业务逻辑比作“做牛排”,那么中间件就是:

  1. 入单服务员:确认这单是谁点的(身份验证)。
  2. 收银员:看这人能不能吃这牛排(权限验证)。
  3. 摆盘师:把牛排端上去之前,摆个盘(日志记录)。

关键点来了: 摆盘师(中间件)不管牛排熟没熟(业务逻辑),他只管摆盘。如果你把“检查牛排熟度”的逻辑写在摆盘师的代码里,那就是屎山代码。

第二章:PSR-15——我们的通用语言

PHP 之所以能流行,除了语法简单,还有一个很重要的原因:标准化。但早期框架五花八门,导致中间件接口也是五花八门。Laravel 有自己的,Symfony 有自己的,Slim 又有自己的。

为了解决这个问题,PHP-FIG 组织祭出了PSR-15(HTTP Server Request Handler)标准。这玩意儿就像是“世界语”。

PSR-15 规定了两个接口:

  1. MiddlewareInterface:所有中间件都要实现这个接口。它有三个方法:process(Request, RequestHandler)
  2. RequestHandlerInterface:它代表“下一个要处理的环节”,或者“处理完所有中间件后的最终响应对象”。

一旦你实现了这两个接口,你的中间件就是“独立宣言”的签署者。它不再依赖任何框架。它只依赖一个事实:输入是一个 Request,输出必须是一个 Response,中间可以做任何事。

让我们来看一段标准的中间件代码。这段代码在任何框架下都能跑,只要它遵循 PSR-7(HTTP 消息接口)和 PSR-15。

<?php

namespace AppMiddleware;

use PsrHttpMessageResponseInterface as Response;
use PsrHttpMessageServerRequestInterface as Request;
use PsrHttpServerRequestHandlerInterface as RequestHandler;
use PsrHttpServerMiddlewareInterface as Middleware;

class CrossOriginMiddleware implements Middleware
{
    public function process(Request $request, RequestHandler $handler): Response
    {
        // 1. 这里的逻辑是通用的,不关心你后面是 Laravel 还是 Slim
        $response = $handler->handle($request);

        // 2. 标准化 CORS 处理
        return $response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
            ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    }
}

看懂了吗?这段代码里没有 $app->get,没有 Route::,没有 Auth::guard。它只处理 HTTP 协议层面的通用问题。这就是标准化的第一步:隔离框架代码与通用代码。

第三章:跨框架的“适配器模式”

光有中间件还不够。我们还需要解决一个更深层的问题:业务逻辑依赖

业务逻辑里通常离不开数据库操作、Redis 缓存、发送邮件。这些操作在不同框架里的写法天差地别。

  • Laravel 里你可能用 DB::table('users')->get()
  • Slim 里你可能直接用 PDO。
  • CodeIgniter 里你可能用 $this->db->query()

如果你把直接调用数据库的逻辑写进了中间件,那你的中间件就“死”了,因为它被框架特定的 ORM 绑定死了。

专家建议:中间件只做“流程控制”,不做“数据操作”。

我们要引入一个抽象层,或者叫“适配器层”。这个层负责把框架特定的数据库连接,转变成一个通用的接口。

假设我们有一个通用的 UserService,它不关心底层的 PDO 是怎么来的,也不关心是 Laravel 的 DB 还是 Doctrine。

namespace AppServices;

class UserService
{
    // 我们注入一个接口,而不是具体的类
    private $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function getCurrentUser()
    {
        // 这里的业务逻辑是标准的,不管底层怎么实现的
        $userId = $_SESSION['user_id'] ?? 0;
        return $this->userRepository->find($userId);
    }
}

然后,我们在框架配置文件里,把这个框架特定的 Repository 实现注入进去。

  • Laravel 配置:注入 IlluminateDatabaseEloquentModel 的实现。
  • Slim 配置:注入一个 PDO 查询的封装类。

这样一来,你的中间件只需要:

public function process(Request $request, RequestHandler $handler): Response
{
    $userService = new UserService($container->get('user_repo'));
    $user = $userService->getCurrentUser();

    // 继续处理...
    $response = $handler->handle($request);

    return $response;
}

看,中间件变得非常干净。它只负责调用 UserService,而 UserService 才是真正干活的。这就是关注点分离的最高境界。

第四章:实战演练——构建一个“通用日志中间件”

为了让你们彻底理解,我们来实战写一个日志中间件。这玩意儿通常每个框架都要写一遍,而且写法千奇百怪。让我们把它标准化。

需求: 记录请求的 URI、耗时、以及用户 ID。无论在哪个框架,格式要一样,日志路径要一样。

标准化中间件:

<?php

namespace AppMiddleware;

use PsrHttpMessageResponseInterface as Response;
use PsrHttpMessageServerRequestInterface as Request;
use PsrHttpServerRequestHandlerInterface as RequestHandler;
use PsrHttpServerMiddlewareInterface as Middleware;

class AuditLoggingMiddleware implements Middleware
{
    // 我们需要注入一个通用的 Logger 接口,而不是框架特定的 Log facade
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function process(Request $request, RequestHandler $handler): Response
    {
        $startTime = microtime(true);

        // 在请求开始前记录
        $this->logger->info('Request Start', [
            'method' => $request->getMethod(),
            'uri'    => (string) $request->getUri(),
            'ip'     => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'
        ]);

        try {
            // 交给下一个处理器(可能是路由,也可能是下一个中间件)
            $response = $handler->handle($request);

            // 计算耗时
            $duration = round((microtime(true) - $startTime) * 1000, 2);

            // 在请求结束后记录(带上状态码)
            $this->logger->info('Request End', [
                'status_code' => $response->getStatusCode(),
                'duration'    => $duration . 'ms'
            ]);

            return $response;

        } catch (Exception $e) {
            // 异常处理也要标准化
            $duration = round((microtime(true) - $startTime) * 1000, 2);
            $this->logger->error('Request Failed', [
                'exception' => get_class($e),
                'message'   => $e->getMessage(),
                'duration'  => $duration . 'ms'
            ]);

            // 抛出异常让框架层处理,或者返回标准的错误响应
            throw $e;
        }
    }
}

注意到了吗?这个中间件里,完全没有任何 $app->middleware 或者 Route::middleware 的痕迹。它只依赖 PSR-15 接口和 PSR-3 (Logger Interface)。

你只需要在 Laravel 的 bootstrap/app.php 里注册它,在 Slim 的 AppFactory 里注册它,在 CodeIgniter 的 Middleware 类里实现它。但逻辑本身,是永久可复用的。

第五章:中间件的“洋葱”堆叠与分组

当你的系统变大了,你需要一层又一层的中间件。认证 -> 权限 -> 日志 -> 限流 -> 性能分析。

这时候,中间件的“管道模式”就非常重要了。PSR-15 标准很好地支持了这种堆叠。

业务逻辑标准化中的陷阱:

千万不要在中间件里写复杂的业务判断!

  • 错误if ($user->role == 'admin') { ... } —— 这是业务逻辑,应该在 Controller 或 Service 里。
  • 正确if ($user->hasPermission('admin_panel')) { ... } —— 这是流程控制,属于中间件的职责。

我们可以写一个“权限检查中间件”,它不关心权限的具体定义,它只关心:这个人是否有资格通过这道门?

namespace AppMiddleware;

use AppServicesPermissionService;

class RoleBasedAccessMiddleware implements Middleware
{
    private $permissionService;

    public function __construct(PermissionService $permissionService)
    {
        $this->permissionService = $permissionService;
    }

    public function process(Request $request, RequestHandler $handler): Response
    {
        // 1. 获取当前请求的路由信息(这部分不同框架处理方式不同,需要适配)
        $route = $request->getAttribute('route'); 

        // 2. 定义该路由需要的权限
        $requiredRole = $route->getOption('require_role'); // 假设我们从路由配置里拿

        if (!$requiredRole) {
            // 如果路由没要求,直接放行
            return $handler->handle($request);
        }

        // 3. 检查用户权限
        if (!$this->permissionService->hasRole($requiredRole)) {
            // 标准化返回 403 Forbidden
            $response = new SlimPsr7Response();
            $response->getBody()->write(json_encode(['error' => 'Forbidden']));
            return $response->withStatus(403);
        }

        return $handler->handle($request);
    }
}

注意到了吗?中间件处理了“用户没有权限怎么办”的流程,但是具体的“权限怎么查”、“角色有哪些”,都在 PermissionService 里。

这种模式允许你在不同框架间移植时,只移植中间件的注册代码,而不需要移植任何业务逻辑代码。

第六章:处理多框架下的“生命周期”差异

这可能是最难的一点。PHP 是无状态的,但在不同框架里,HTTP 请求的生命周期差异巨大。

  • Laravel:有 App::booted,有 Kernel,请求周期长,有很多全局钩子。
  • Slim:非常轻量,生命周期短,基本就是 Request -> Handler -> Response。
  • Swoole/Workerman:长连接,进程常驻内存。

如果你要把业务逻辑标准化,你必须把这些“框架特性”封装掉。

比如,有一个“限流(Rate Limiting)”中间件。
在 Laravel 里,你可能需要每分钟保存 N 个请求到 Redis。
在 Slim 里,你可能需要每分钟保存 N 个请求到 Redis。
在 Swoole 里,你可能需要每个进程保存,并且要考虑多进程锁。

标准化方案:

中间件只负责记录“我是谁,我想干嘛”。至于“我是怎么存数据的”,交给底层的 StorageAdapter

public function process(Request $request, RequestHandler $handler): Response
{
    $ip = $request->getServerParams()['REMOTE_ADDR'];

    // 这里的 key 是通用的,不管你怎么存
    $this->counter->increment('api_request_count', $ip);

    $limit = $this->config->get('rate_limit');
    $current = $this->counter->get('api_request_count', $ip);

    if ($current > $limit) {
        return $this->createErrorResponse('Too Many Requests');
    }

    return $handler->handle($request);
}

对于 Swoole,StorageAdapter 可能是共享内存中的计数器;对于 Slim,它可能就是 Redis。业务逻辑(限流算法)是不变的,只有存储介质变了。

第七章:不要让中间件变成“上帝类”

在结束之前,我要给你们泼一盆冷水。很多新手在实现标准化时,会犯一个错误:把所有逻辑都塞进中间件。

他们写了一个 UniversalMiddleware,里面包含了:

  1. 解析 JWT。
  2. 解析 SOAP。
  3. 查询用户。
  4. 格式化响应。
  5. 记录 SQL 慢查询。
  6. 检查 HTTPS。

这不行!这绝对是反模式。

专家级设计原则:单一职责。

中间件应该只做一件事,或者几件非常相关的事。我们要把 UniversalMiddleware 拆解:

  1. AuthenticationMiddleware:只管鉴权,返回 User 对象。
  2. BodyParserMiddleware:只管把 JSON 解析成对象。
  3. JsonResponseMiddleware:只管把对象序列化成 JSON。
  4. PerformanceMonitorMiddleware:只管计时。

在你的业务逻辑代码(Service 层)里,把这些中间件串联起来。这样,当你在 Laravel 里复用这段代码到 Slim 时,你可以自己组合需要的中间件,而不需要买一送一堆你用不上的逻辑。

第八章:最终架构图解(脑补版)

想象一下你的系统架构:

  1. 最底层:框架层。Laravel 的 Kernel, Slim 的 Middleware, CodeIgniter 的 Hooks。它们就像房子的地基,形状各异,但都用来支撑房子。
  2. 中间层标准接口层。也就是我们刚才写的那些实现 PSR-15 的中间件。
  3. 应用层业务逻辑层。也就是你的 Service,你的 Repository,你的 Domain Logic。这是房子的主体。
  4. 最上层:框架适配器。负责把框架特定的对象,转换成标准接口层的对象。

流程是这样的:
HTTP 请求进来 -> 框架层处理 -> 抛给标准接口层中间件 -> 标准接口层调用应用层 -> 应用层处理 -> 返回给标准接口层 -> 返回给框架层。

你看,中间层(标准接口层)和应用层(业务逻辑层)是互不认识的,它们甚至都不知道对方是谁。它们只知道通过“接口”对话。这就是为什么它们可以跨框架移植。

第九章:为什么这是终极解决方案?

你可能问:“直接用 PHP 纯原生写不就行了吗?干嘛还要中间件?”

纯原生写法当然可以,但那是 2010 年的做法。现代 PHP 开发强调的是组合解耦

使用中间件模式标准化,带来的好处是显而易见的:

  1. 降本增效:开发新项目时,直接把去年的中间件库复制过来,甚至不用改一行代码。你节省的是每天重复造轮子的时间。
  2. 团队协作:如果你的团队里,有人负责 Laravel,有人负责 Slim,但他们要维护同一套业务规则(比如支付逻辑、会员体系),中间件模式让你们有了共同语言。Laravel 开发者写的中间件,Slim 开发者可以直接用。
  3. 测试友好:标准的中间件是可以单独测试的。你可以写一个单元测试,传入一个 Mock Request,看看中间件返回的 Response 是否符合预期。这比测试一个庞大的 Controller 要简单得多。

第十章:代码重构指南——如何迁移你的屎山

最后,我们来看一个具体的重构步骤。

假设你现在有一堆写在 Laravel Controller 里的业务逻辑,比如一个 UserController。里面有 500 行代码,混杂了 DB 查询、Email 发送、权限判断、HTML 输出。

第一步:提取业务逻辑。
把核心的“获取用户列表”逻辑提取到 UserListService 里。这个 Service 不依赖任何框架,只依赖标准接口。

第二步:创建中间件。
创建一个 UserListMiddleware

  • 在中间件里,初始化 UserListService
  • 调用 Service 获取数据。
  • 把数据存入 Request 的 Attribute 里,方便后续读取。

第三步:迁移。
在你的 Slim 项目里,引入相同的 UserListMiddleware

你会发现,Slim 的 Controller 变得非常简单:

public function list(Request $request, Response $response) {
    // 数据已经在中间件里准备好了,直接取就行
    $users = $request->getAttribute('users');

    $response->getBody()->write(json_encode($users));
    return $response;
}

原本那个 500 行的 Laravel Controller,现在变成了两个 100 行的文件。这就是重构的艺术。

结语:拥抱通用,拒绝锁死

各位,PHP 生态系统之所以强大,是因为它既有灵活性的“泥巴”,又有标准化的“水泥”。

中间件模式就是我们用来砌墙的水泥。它让我们的业务逻辑像砖块一样,可以在不同的框架之间自由搬运、拼接、堆叠。

不要让你的代码被某个框架绑架。当你写下一行 new Request(),或者实现一个 process 方法时,请保持一种心态:我是一个跨平台的通用组件,我是为了解决业务问题而存在的,而不是为了解决框架问题。

当你做到了这一点,你就是一名真正的 PHP 架构师。哪怕你明天被老板逼着从 Laravel 换到 Swoole,你也能淡定地喝口咖啡,说一句:“呵,无非就是换个中间件注册的地方。”

好了,今天的讲座就到这里。下面是提问环节,我不接受“这代码能不能跑”这种问题,我只接受“如何把它集成到 .NET Core 里”这种挑战性的问题。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注