PHP公共模块抽取原则

wen PHP项目 3

本文目录导读:

PHP公共模块抽取原则

  1. 核心架构原则
  2. PHP 工程实现原则(具体细节)
  3. 命名与依赖管理原则
  4. 实战反例诊断(哪些不该抽?)
  5. 总结:判断是否抽取成功的“三看”清单

在PHP开发中,抽取公共模块(或公共代码)的核心原则可以概括为:“高内聚、低耦合” + “避免重复(DRY)” + “职责单一(SRP)”

但实际落地时,针对不同粒度(函数、类、包、服务),有很多具体的坑要避开,以下是PHP特有的、且实用性极强的公共模块抽取原则:

核心架构原则

依赖倒置(面向接口,而非具体实现)

  • 原则:抽取公共模块时,不要让上层业务依赖底层模块的具体类,而应依赖其接口或抽象类。
  • PHP实践:使用 Composer 管理包时,通过 composer.json 中的 require 引入接口包,具体实现由容器(DI Container)注入,这样,当底层从 Redis 换成 Memcached 时,上层业务代码零改动。

优先组合,而不是继承

  • 原则:Java/C++ 喜欢用抽象类(Abstract Class)来抽取公共逻辑,但 PHP(尤其是现代PHP)更推荐使用 Trait组合模式
  • PHP具体场景
    • Trait:用于抽取出多个不相关类中的重复方法(例如日志记录、权限校验),虽然好,但不要滥用,Trait 用多了会导致代码难以追踪。
    • 组合/服务类:如果一个模块有状态(比如数据库连接、文件句柄),应该拆成一个独立的 Service 类(如 LoggerService),通过构造函数注入,而不是让业务类去继承它。

单一职责(Single Responsibility Principle)

  • 原则:一个公共模块只做一件事,并且做好。
  • PHP反例:不要创建一个 CommonHelper.php 文件,里面塞满了 formatDate()sendSms()generateToken() 这些毫无关联的函数,这种“上帝类”很难测试和维护。
  • 正解:拆分为 DateHelperSmsNotifierTokenService

开闭原则(Open-Closed)

  • 原则:对扩展开放,对修改关闭。
  • PHP实践:公共模块抽出来后,需求变更时应通过新增代码(如策略模式、回调函数)来扩展,而不是直接修改公共模块的代码本身。

PHP 工程实现原则(具体细节)

避免“万能”函数文件

  • 错误:有些项目习惯建立一个 app/helpers.php,把乱七八糟的全局函数塞进去,并用 Composer 的 files 自动加载。
  • 正确:尽量用静态方法门面(Facade),除非是极简的数组操作或字符串操作(如 array_get),函数文件一旦多了,命名冲突(尤其和第三方包冲突)是致命的。

明确“公共”的边界(上下文无关)

  • 原则:抽出来的模块不能携带具体业务上下文。
  • PHP反例:在 Common/ExcelExport 模块中,exportUserList 方法里包含 WHERE user_status = 1 这样的SQL,这就是错误,公共模块只负责接收参数并输出文件,具体查询逻辑应留在业务层调用。

配置外部化(Config Externalization)

  • 原则:公共模块中不要写死任何环境相关的常数(如API Key、数据库表名、文件路径)。
  • PHP实践:使用环境变量(getenv()$_ENV),通过依赖注入的方式将配置传递给公共模块。

分层的粒度控制(不要过度抽取)

  • 应用层(Laravel/ThinkPHP):只抽取跨项目的通用逻辑(如支付接口、物流查询、统一响应格式)。不要把业务表字段的格式化逻辑抽到公共层。
  • 基础层(框架无关):抽取纯逻辑(日期计算、字符串加密、设计模式组件),这部分则一定要保证和框架完全解耦(不依赖 IlluminateThinkPHP 的类)。

命名与依赖管理原则

依赖关系严格定向

  • 公共模块 不允许 反向依赖业务模块。
  • 依赖方向:业务代码 -> 业务公共模块 -> 基础公共模块 -> 框架/Vendor包

命名空间(Namespace)的统一

  • 抽取的模块要有固定的命名空间前缀,App\Lib\App\Support\
  • 防止和 Vendor 中的全局命名空间冲突,避免简单的 前缀命名(如 class _Common 是古老的PHP4风格,不推荐)。

用 Composer 进行物理隔离(重要)

  • 如果项目是多应用程序(如管理后台 + API + 前端网站),应将公共模块抽取为独立的内部 Composer 包(private repository),而不是用 git submodule 或复制目录。
  • 这能让版本管理更清晰(require acme/common: ^1.2)。

实战反例诊断(哪些不该抽?)

  1. “看起来像”但不属于公共的业务规则
    • UserModel::getGold() 方法,如果只有这一个项目使用,它属于业务模型,不要抽入公共 Common 目录。
  2. 只出现过一次的代码:如果一个代码块只在两个极端场景下被复用,但逻辑差异巨大,强行通过参数切换只能导致“面条式”代码。
  3. 包含 HTTP 请求或 Session 的逻辑:公共模块严禁直接用 $_SESSION$_GET,如果需要,必须通过参数传递跨层数据。

判断是否抽取成功的“三看”清单

指标 优秀
一看依赖 模块不依赖外部的全局状态,测试时只需new即可。 模块里写死了 DB::table('xxx')file_put_contents
二看调用 调用者只依赖于1个方法或1个职责。 调用者必须传入 $flag=1 或者 $type='admin' 来走不同的分支。
三看变更 修改同步需求时,只需改公共模块1处。 修改了公共模块,导致另外3个无关业务报错(强耦合)。

最后的建议:在PHP中,“不成熟的抽象”(The Wrong Abstraction)比“重复代码”(Duplication)更危险,如果你不确定那个重复逻辑未来会不会变,先复制一份,等第三次出现时再考虑抽取,这往往比一开始就设计过度要好。

抱歉,评论功能暂时关闭!