4.3 子文件接管与 App 扩展
X5 最强的扩展点:让插件完全代替系统的一段逻辑。
4.3.1 目录被重新切过了
X3.5 的模块逻辑散在两个地方:source/module/ 放类,include/ 放过程文件。X5.0 把这套结构重新归类到全新的 MitFrame 框架体系下:
| X3.5 | X5.0 |
|---|---|
source/module/xxx.php |
source/app/xxx/xxx.php |
include/ 里的过程文件 |
source/child/ 与 source/app/xxx/child/ |
| 无 app 概念 | source/app/{模块}/ 一个功能模块一个目录 |
一个 app 目录下有三层:module/ 放类文件(原 source/module 的内容)、child/ 放该模块的子文件、template/ 放该模块的模板文件。
这里有个必须记住的结论:X5.0 不再兼容 X3.5 及更早版本。如果你手上有老插件里直接 include source/module/ 的代码,升级后要按新路径重做。
4.3.2 什么是子文件(child)
子文件就是系统的过程文件。X3.5 时代改这些文件只有一条路:直接改源码,然后从此每次升级都要重新打补丁。
X5.0 换了个思路。系统在执行过程中会自动检查对应模块的 child 目录,如果那里有同名文件,就用你的,不用自己的。
| 位置 | 覆盖范围 |
|---|---|
/source/child/ |
全局子文件,影响面最广 |
/source/app/xxx/child/ |
单个 app 的子文件,只影响该模块 |
关键在于这不只是「加一段逻辑」,而是完全代替原有系统逻辑。X3 时代插件只能在钩子前后插一脚,代码始终得在系统主干上跑;X5 里你可以把那段主干整个换掉。这是本书里权限最高、也最容易翻车的扩展点。
4.3.3 App 扩展:在 app 下建同名文件
既然 app 目录下的文件按名字被引用,那扩展方式就直白了:在 /source/app 下自建目录,创建同名文件。
source/app/xxx/
├── xxx.php ← 扩展这个模块的入口
├── child/ ← 这个模块的子文件
└── template/ ← 这个模块的模板
拿到某个模块的文件后,用 appfile() 加载其他 app 下的文件:
<?php
require './source/class/class_core.php';
$discuz = C::app();
$discuz->cachelist = [];
$discuz->init();
require_once appfile('yyy/zzz'); // 调用 /source/app/xxx/yyy/zzz.php
appfile() 的参数是相对 /source/app/ 的路径,不带扩展名。
app 内可以直接放模板文件,渲染方式和插件模板不同——要显式给出目录:
template('tpl', 0, 'source/app/xxx/template'); // 仅支持 .php 格式模板
第三个参数是模板目录,第二个参数是样式 ID(传 0 表示用默认)。这条路径只支持 .php 格式的模板,.htm 不行——和 source/app/xxx/template/ 这个目录名给人的直觉相反,实际用的时候别顺手丢个 .htm 进去。
后台有 app 管理页,URL 形式是:
admin.php?action=app&operation=
4.3.4 接管之前:必须做的调研
子文件接管的权限太大,上手前有三件事必须做完,不然出了问题会连原因都定位不到。
第一,读原child 文件。 打开 /source/app/xxx/child/ 下你要替换的那个文件,逐行读完。它的入参从哪来、返回什么、异常怎么抛、被谁 try 住了,全要知道。很多 child 文件很小,就几十行,一分钟能读完;但里面往往藏着隐式依赖——比如它用了某个全局变量,那个变量是调用方临时塞进去的。
第二,看调用方。 grep 一下这个文件名在 source/ 下哪些地方出现,调用点有几个。知道谁调它,才知道改坏之后会波及哪些页面。
第三,准备回滚。 接管前把原文件复制一份放到插件目录里,出问题立刻换回去。升级前把改动写成文档,记清动了哪些模块——下次官方升级会覆盖 source/app/,你得知道哪些文件要重新放。
| 调研项 | 要产出的结论 |
|---|---|
| 原child 文件 | 入参、返回值、依赖的全局变量 |
| 调用方 | grep 结果清单,列出受影响页面 |
| 回滚方案 | 原文件备份位置、恢复步骤 |
准备停服在测试站点上跑一遍完整业务流程,比事后翻日志快得多。
4.3.5 接管时该做什么
真要接管的时候,思路不是「重写」,而是「先跑通原逻辑,再改」。具体说:
在 source/app/xxx/child/ 下建同名文件,先把原文件的逻辑整段复制过来,确认站点功能一切正常。这一步走通说明你找对了文件、路径也写对了。之后再逐段改你关心的部分,每改一段测一次。
这样做的好处有两个:出问题时能立刻区分是「路径放错了」还是「业务逻辑写错了」;万一功能需要撤回,把备份换回去就完事。
用 $_G['xxx'] 存跨调用的临时状态时注意清理。child 文件可能被多次引用,留脏数据会让后面的逻辑判断出错。涉及数据的地方照例用 C::t('xxx')->... 或 DB::table('tablename'),别自己拼表名前缀。
4.3.6 和钩子的分工
| 需求 | 用什么 |
|---|---|
| 在某一步前后插一段逻辑 | 钩子({hook/钩子名} + plugin_ 类) |
| 改一个设置项的值 | 插件变量 |
| 整个替换系统的一段逻辑 | 子文件接管 |
| 给某个模块加一个新的动作文件 | app 扩展 |
多数需求钩子就够了。接管是最后手段——因为它把系统代码从「别人的代码」变成了「你的代码」,官方升级时你得跟着改。
判断标准很简单:如果这段逻辑系统更新时可能变,就别接管;如果它十年都不会变,而你的需求很特殊,接管反而省事。
4.3.7 收尾检查
- 路径用的是 X5 的
source/app/,不是 X3.5 的source/module/。 - child 文件放对了层级:全局的进
/source/child/,模块的进/source/app/xxx/child/。 - 原文件已备份,回滚步骤写在文档里。
appfile()的参数不带扩展名,路径相对/source/app/。- app 模板用的是
.php格式,template()第三个参数给了目录。 - 跨调用状态用完清理干净。
子文件接管让插件有了「改系统」的能力,也意味着要承担维护成本。下一节讲的两个扩展点更省心——不改逻辑,只换组件。
