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() 第三个参数给了目录。
  • 跨调用状态用完清理干净。

子文件接管让插件有了「改系统」的能力,也意味着要承担维护成本。下一节讲的两个扩展点更省心——不改逻辑,只换组件。