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

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

各位 PHP 开发者,大家好。

今天我们要聊的不是“怎么写一个 Hello World”,也不是“为什么 array_keysforeach 快”。我们要聊的是——如何从框架的束缚中逃离出来,哪怕你的项目里混着 Laravel、Symfony、Slim,甚至是一个还在维护的 CodeIgniter,你的核心业务逻辑却依然能保持纯洁、统一,且优雅得像个贵族。

很多开发者,特别是初级开发者,喜欢把业务逻辑写在控制器里。这就像把你的私房菜直接端到路边摊的桌子上,看着就乱。更糟糕的是,当老板说:“我们要把用户系统从 Laravel 迁移到 Slim”,或者“我们要把旧系统的 PHP5.6 代码重构到 PHP 8.3”,你看着那些充斥着 $this->request->input('email')$this->response->withJson() 的代码,是不是感觉天都要塌了?你的业务逻辑,被框架的“口水”泡发了,已经失去了原本的味道。

今天,我们就来通过中间件模式,构建一座桥梁,把业务逻辑从泥潭中拔出来,进行物理隔离,然后优雅地插到任何我们想要的框架里。

第一部分:框架的“混乱哲学”与我们的“洁癖”

PHP 生态系统是一个混乱但充满活力的地方。你可能早上还在写 Laravel 的 Service Provider,晚上就要去维护一个基于原生 PSR-7 的 Slim 应用。

为什么这很难?因为框架设计哲学不同。
Laravel 认为开发者应该快乐,所以它恨不得帮你把袜子穿好(比如全局 Facade、辅助函数)。
Symfony 认为开发者应该专业,所以它给你提供了庞大的配置文件(YAML/XML/PHP)。
Slim 认为开发者应该极简,所以它几乎什么都不帮你做。

核心痛点: 框架的 HTTP 抽象层千奇百怪。

  • Laravel 有 IlluminateHttpRequestResponse
  • Symfony 有 SymfonyComponentHttpFoundationRequestResponse
  • Zend Diactoros、Laminas、Guzzle Http 都有一套自己的 HTTP 对象定义。

如果你把业务逻辑写死在这些对象上,你的代码就死定了。你的 RegisterService 方法里不能有 request->email,它应该有一个参数叫 $emailData

第二部分:中间件模式——翻译官与守门人

要实现隔离,我们需要引入中间件。但这里的中间件,不是那种简单的“记录日志”或“验证 Token”的简单装饰器。我们这里的中间件,是防腐层

想象一下,业务逻辑(你的核心代码)是一个只会讲“标准普通话”的人。而 Laravel、Symfony 这些框架是讲“方言”的。中间件就是那个翻译官。它听懂方言(框架请求),翻译成普通话(DTO对象),然后丢给核心代码去处理。核心代码处理完,吐出普通话(业务结果),翻译官再把它翻译成方言(框架响应)。

这不仅仅是逻辑上的隔离,更是物理上的隔离。我们的核心业务代码文件夹里,绝对不能出现任何框架的 use 引用。

第三部分:物理隔离的代码架构设计

让我们来构建一个标准的架构图(脑海中想象一下):

/src
   /Domain          <-- 核心业务逻辑,没有框架依赖
   /Application     <-- 用例、服务、DTO
   /Infrastructure  <-- 适配器:Laravel, Symfony, Slim, Doctrine

注意,Domain 和 Application 层,是纯净的。它们不认识 IlluminateHttp,也不认识 SlimHttp

第四部分:实战演练——构建一个“跨框架”的登录系统

为了演示,我们假设我们要实现一个用户登录功能。
核心逻辑:

  1. 接收邮箱和密码。
  2. 验证邮箱格式(业务规则)。
  3. 验证密码强度(业务规则)。
  4. 查数据库验证用户。
  5. 返回 Token。

这个逻辑在任何框架下都是一样的。我们不要让它变。

1. 定义标准契约(Input 和 Output)

首先,我们要定义标准的输入和输出对象。这就是我们的“普通话”。

src/Application/DTO/LoginRequest.php

<?php

namespace AppApplicationDTO;

class LoginRequest
{
    private string $email;
    private string $password;

    // 简单的构造函数,不包含逻辑
    public function __construct(string $email, string $password)
    {
        // 这里甚至可以加一些简单的验证
        if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException('Invalid email format');
        }

        if (strlen($password) < 6) {
            throw new InvalidArgumentException('Password too weak');
        }

        $this->email = $email;
        $this->password = $password;
    }

    // Getter
    public function getEmail(): string { return $this->email; }
    public function getPassword(): string { return $this->password; }
}

src/Application/DTO/LoginResponse.php

<?php

namespace AppApplicationDTO;

class LoginResponse
{
    private string $token;
    private string $username;

    public function __construct(string $token, string $username)
    {
        $this->token = $token;
        $this->username = $username;
    }

    public function toArray(): array
    {
        return [
            'token' => $this->token,
            'user' => $this->username
        ];
    }
}

2. 核心业务逻辑(无框架依赖)

现在,这是你的核心代码。看,它干干净净,没有任何框架的气息。

src/Domain/Services/AuthService.php

<?php

namespace AppDomainServices;

use AppApplicationDTOLoginRequest;
use AppApplicationDTOLoginResponse;

// 注意:这里没有 use IlluminateHttpRequest,也没有 use PsrHttpMessageResponseInterface
class AuthService
{
    // 注入一个数据库接口,而不是具体的 PDO
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private TokenGeneratorInterface $tokenGenerator
    ) {
    }

    public function login(LoginRequest $request): LoginResponse
    {
        // 1. 获取用户
        $user = $this->userRepository->findByEmail($request->getEmail());

        if (!$user) {
            throw new DomainException('User not found');
        }

        // 2. 验证密码
        if (!$user->validatePassword($request->getPassword())) {
            throw new DomainException('Invalid credentials');
        }

        // 3. 生成 Token
        $token = $this->tokenGenerator->generate($user->getId());

        // 4. 返回标准结果
        return new LoginResponse($token, $user->getName());
    }
}

这里我们用到了依赖倒置原则AuthRepositoryInterfaceTokenGeneratorInterface 是标准接口,真正的实现(比如基于 PDO 或 Redis 的)可以在 Infrastructure 层写。

3. 适配器层——中间件

现在,怎么把这个纯净的 AuthService 和那个满嘴方言的框架连起来?靠中间件。

假设我们有一个基于 PSR-15 的中间件接口。

src/Infrastructure/Adapters/Laravel/LoginMiddleware.php

<?php

namespace AppInfrastructureAdaptersLaravel;

use AppApplicationDTOLoginRequest;
use AppApplicationDTOLoginResponse;
use AppDomainServicesAuthService;
use Closure;
use IlluminateHttpRequest;
use IlluminateHttpResponse;

class LoginMiddleware
{
    public function __construct(
        private AuthService $authService
    ) {
    }

    /**
     * @param Request $request
     * @param Closure $next
     * @return Response
     */
    public function handle(Request $request, Closure $next)
    {
        // 1. 翻译:从框架方言翻译成标准普通话
        try {
            $loginRequest = new LoginRequest(
                $request->input('email'),
                $request->input('password')
            );
        } catch (InvalidArgumentException $e) {
            // 将业务异常翻译成 HTTP 422 Unprocessable Entity
            return response()->json(['error' => $e->getMessage()], 422);
        }

        // 2. 执行:调用纯净的业务逻辑
        try {
            $loginResponse = $this->authService->login($loginRequest);
        } catch (DomainException $e) {
            // 将业务异常翻译成 HTTP 401 Unauthorized
            return response()->json(['error' => $e->getMessage()], 401);
        }

        // 3. 翻译:将标准普通话翻译成框架方言
        return response()->json($loginResponse->toArray());
    }
}

看看这段代码,是不是很优雅?

  • 中间件只关心输入输出
  • 中间件负责处理异常
  • 中间件负责处理HTTP 状态码
  • 中间件不知道 AuthService 是怎么存数据的,也不知道 Token 是怎么生成的,它只管把它拿来用。

4. 另一个框架:Slim

现在,老板说:“Slim 也需要这个登录功能,但 Slim 很轻量,别用 Laravel 那套。”

你不需要重写 AuthService。你只需要写一个新的中间件,甚至可以复用上面的代码逻辑。

src/Infrastructure/Adapters/Slim/LoginMiddleware.php

<?php

use PsrHttpMessageResponseInterface as Response;
use PsrHttpMessageServerRequestInterface as Request;
use SlimPsr7Response as SlimResponse;
use AppApplicationDTOLoginRequest;
use AppDomainServicesAuthService;

class SlimLoginMiddleware
{
    private AuthService $authService;

    public function __construct(AuthService $authService)
    {
        $this->authService = $authService;
    }

    public function __invoke(Request $request, Response $response, $next)
    {
        // 1. 翻译
        $body = $request->getParsedBody();

        if (!isset($body['email']) || !isset($body['password'])) {
            $response->getBody()->write(json_encode(['error' => 'Missing fields']));
            return $response->withStatus(422);
        }

        try {
            $loginRequest = new LoginRequest($body['email'], $body['password']);
            $loginResponse = $this->authService->login($loginRequest);
        } catch (Exception $e) {
            $response->getBody()->write(json_encode(['error' => $e->getMessage()]));
            return $response->withStatus(401);
        }

        // 2. 翻译
        $response->getBody()->write(json_encode($loginResponse->toArray()));

        return $response->withHeader('Content-Type', 'application/json');
    }
}

看,虽然中间件的具体写法因为 Slim 的轻量化略有不同(比如它没有 $request->input() 这种魔法方法,我们要手动读 ParsedBody),但核心业务逻辑 AuthService 一行都没变!

第五部分:深入探讨——为什么这是“专家级”设计?

很多开发者会反驳:“不就是个登录吗?我直接在控制器里写不就完了吗?”

这就是“初级工程师”和“专家”的区别。

1. 物理隔离 vs 逻辑松散

很多项目虽然也分层,但把控制器放在 App/Http/Controllers,模型放在 App/Models,逻辑混在了一起。一旦你要换框架,你会发现改模型里的代码比改控制器还难,因为模型里偷偷引用了框架的类。

专家的做法: 核心业务逻辑所在的目录,不能被任何框架的命名空间污染。我们的 src/Domainsrc/Application 目录是禁地,外人(框架)进不去。框架只能通过接口(门)进去。这就是真正的物理隔离。

2. 处理“依赖地狱”的终极解药

在大型系统中,我们的 AuthService 可能依赖 OrderServiceOrderService 依赖 PaymentService,这很正常。但是,如果我们不使用依赖注入容器,而是使用 new Class(),当你要把这个 Service 换个框架时,你会崩溃,因为新框架可能不支持你那样注册类。

通过中间件,我们只暴露 AuthService 这个单一接口给框架。框架只知道怎么把 HTTP Request 变成 LoginRequest,然后调用 login()

容器配置(伪代码):

  • Laravel: App::bind(AuthServiceInterface::class, AppDomainServicesAuthService::class);
  • Symfony: $container->register('auth_service', AppDomainServicesAuthService::class);
  • Slim: $app->add(new SlimLoginMiddleware($container->get(AuthService::class)));

框架只需关注如何“组装”这个管道,而不需要理解管道内部的业务逻辑细节。

3. 值对象与不可变性

再看我们的 LoginRequest

class LoginRequest {
    private string $email;
    private string $password;
    // 没有 Setter!只有构造器!
}

在框架的 Request 对象里,属性是可变的。业务逻辑里,一旦对象创建,邮箱就不应该变了。这种不可变性是现代编程的重要趋势,而中间件模式天然适合在这里发挥作用——在进入核心逻辑之前,将不可变的 DTO 传进去。

第六部分:进阶技巧——面对“遗留代码”的暴力美学

有时候,你接手了一个旧项目。那个项目是一个几千行的 PHP 文件,里面全是 if ($_GET['action'] == 'login']) { ... }。没有 MVC,没有中间件,甚至没有 Composer。

这时候,物理隔离显得尤为珍贵。

我们要用中间件“包裹”住这个烂摊子。

// 这是一个纯 PHP 脚本,没有框架
require_once 'legacy_auth_code.php'; // 那个几千行的文件

function legacyAuthHandler() {
    // 假设 legacy 代码通过全局变量或者超级全局变量获取数据
    $email = $_POST['email'];
    $pass = $_POST['password'];

    // 调用旧逻辑
    $isValid = authenticate_legacy_user($email, $pass);

    if ($isValid) {
        $_SESSION['logged_in'] = true;
        header('Location: dashboard.php');
    } else {
        echo "Login Failed";
    }
}

// 现在我们在中间件层做隔离
// src/Adapters/NoFramework/AuthMiddleware.php
function noFrameworkMiddleware($action) {
    if ($action === 'login') {
        // 假设有一个全局的路由解析器把 action 传进来了
        legacyAuthHandler();
        exit;
    }
    // ...
}

虽然这看起来有点“黑客”手段,但它的核心思想是一样的:把不干净的逻辑(旧代码)封印在一个函数里,通过中间件(路由控制)来调用它,并通过 DTO(模拟输入输出)来保证接口的稳定。

第七部分:关于性能与可测试性

有人可能会说:“加了一层中间件,再加了一层 DTO,岂不是性能损耗巨大?”

这是一个典型的“过早优化”心态。在 PHP 这种解释型语言中,函数调用的开销相对于数据库查询、网络 IO 来说是微不足道的。你通过增加一个函数调用获得的代码可维护性可测试性(你可以直接 new LoginRequest 进行单元测试,不需要启动 HTTP 服务器)和架构清晰度,远远超过了那一点点微不足道的性能损失。

第八部分:总结与思考

各位,通过中间件模式实现业务逻辑标准化与物理隔离,不仅仅是写代码的方式,更是一种设计哲学

它要求你:

  1. 克制:不要在业务代码里直接操作 HTTP 对象。
  2. 抽象:多定义接口,少写具体类。
  3. 隔离:把框架当成一个可以随时丢弃的“皮肤”,把业务逻辑当成那个永远不变的“灵魂”。

当你把 AuthRepositoryTokenGenerator 实现出来放在 Infrastructure 层时,你会发现你的代码变得非常有条理。

  • 修改业务逻辑?去 src/Domain
  • 修改数据库?去 src/Infrastructure/PDO
  • 修改接口返回格式?去 Middleware

真正的专家,不是写出最炫酷的代码的人,而是写出那些让人“无法修改”的人——当然,是指那些看起来不可修改、实际上逻辑清晰、结构完美的代码。

下次当你打开一个新的 Laravel 项目,准备在 Controller 里写业务逻辑时,请先停下来,想一想:“我是想继续做那个被框架绑住的奴隶,还是想成为那个掌控框架的指挥官?”

中间件,就是你手中的指挥棒。

好,今天的讲座就到这里。去写你的纯净业务逻辑吧,别回头。

发表回复

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