ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Yii2 事件机制:EVENT_BEFORE_ACTION 的执行链路与工程实践

Yii2 事件机制:EVENT_BEFORE_ACTION 的执行链路与工程实践 打开 Yii2 源码之前我一直把EVENT_BEFORE_ACTION当成一个“约定俗成的钩子”在behaviors()里挂上去事件就能在action执行前自动跑一圈。直到有一次排查线上权限失效的问题我才发现这玩意儿的底层真相比我想象中要简单也比我想象中要深。事件机制的代码量不多但牵一发而动全身。EVENT_BEFORE_ACTION本质上不是某个神秘的框架魔法它就是一个被Controller声明的常量一个被trigger()方法触发的事件名。真正搞懂它需要捋清楚三件事它在一个请求的生命周期中究竟处于哪个坐标、底层的Component::trigger()是如何把事件分发出去的、它和beforeAction()这个方法之间到底是什么关系。这篇文章按“庖丁解牛”的方式拆给你看。阅读提示文中会贴不少源码片段但不会贴整段完整文件而是把关键行挑出来讲。建议你打开手边的 Yii2 源码对照看路径是vendor/yiisoft/yii2/绝大多数内容在base/Controller.php、base/Component.php、base/Module.php、base/Event.php、base/ActionEvent.php这几个文件里。1. EVENT_BEFORE_ACTION 在请求生命周期里的坐标1.1 从入口到 Action 执行那三跳先抛开烦琐的细节只说一个请求从入口进来之后Yii2 框架到底做了什么。入口文件web/index.php加载完应用配置之后会调用$application-run()而run()内部最终走向handleRequest()。这一步做的事是读取路由、解析参数、把请求转化成[路由, 参数]的二元组然后交给runAction()。runAction()本身也是分层的。在Application里执行时它拿到的不一定是最终控制器的路由——如果站点有前台、后台、API 这类不同 Module先到的是模块层级的runAction()然后由模块去创建它内部真正的 Controller 实例并执行。我用一个简单的例子帮你理解这个调用链// 请求 URL: /admin/user/update?id5 // 路由解析结果大致是: admin/user/update // Application 先看到的是 admin 模块对应的路由 $result Yii::$app-runAction(admin/user/update, [id 5]);这段代码执行后内部经过的路径大致如下Application发现admin是一个模块调用Module::runAction(user/update)Module内部创建admin模块下的 UserController 实例模块级runAction()在创建完 Controller 和 Action 对象后执行模块自己的beforeAction()这一步也会触发模块的EVENT_BEFORE_ACTION进入Controller::runAction()runAction()内部再调用Controller::beforeAction($action)这一步触发Controller的EVENT_BEFORE_ACTION网上讨论得最多的基本是第 5 步。它也是让你能够在业务执行前插入逻辑的地方。1.2 控制器里的 beforeAction 方法就是事件触发点打开Controller基类看一下你会发现在它内部并不是runAction()里直接 trigger 事件而是所有action执行前都先经过一个方法叫beforeAction()。这个方法的内容就是触发事件的真正入口。// base/Controller.php public function beforeAction($action) { $event new ActionEvent($action); $this-trigger(self::EVENT_BEFORE_ACTION, $event); return $event-isValid; }这里可以提取出三个关键知识点第一EVENT_BEFORE_ACTION是Controller类的一个常量它的值就是一个普通字符串beforeAction。你甚至可以直接用字符串beforeAction去绑定事件效果一样。只不过用常量写更安全因为如果哪天框架改了事件名的字符串你只需要升级框架代码而不用改业务代码。第二$event是一个ActionEvent实例它被创建时会把当前要执行的$action对象传进去。这也是为什么在事件处理方法里能通过$event-action拿到当前 action 的原因。第三$event-isValid默认是true这个值在事件处理器中被改成false后beforeAction()会返回false从而阻断 action 的执行。理解了这个结构你自然就能明白为什么EVENT_BEFORE_ACTION在文档里总是被说成“在 action 执行之前触发可以通过返回 false 来阻止 action 执行”——因为这句话的准确意思是控制器基类的beforeAction()方法负责弹出一个事件事件处理器可以通过修改$event-isValid来影响beforeAction()的返回值。1.3 三级 beforeAction 对比为了不把模块和控制器这两个容易混淆的层级搞混我整理了一张表格级别方法名实现方式触发事件情况ApplicationbeforeAction($action)方法重写本身不直接 trigger 事件主要用于检查模块链ModulebeforeAction($action)方法重写通过Module::EVENT_BEFORE_ACTION触发事件ControllerbeforeAction($action)方法重写通过Controller::EVENT_BEFORE_ACTION触发事件这里有一个细节值得留意Application的beforeAction()是预留的重写钩子默认返回true不会像Module和Controller那样自动 trigger 事件。如果你希望在全局层面拦截请求最常见的做法不是绑它的EVENT_BEFORE_ACTION而是直接在入口应用类里重写beforeAction()方法或者写一个全局行为挂在 Application 上。搞清楚了坐标接下来我们就要完整呈现一个 action 从生成到执行的全流程。// 伪代码展示 complete flow $action $controller-createAction(update); // 此刻 $controller-action 这个属性还被赋值为这个 action $result null; if ($controller-beforeAction($action)) { // 这里才有 EVENT_BEFORE_ACTION 事件触发 // 若 isValid 为 false就跳过了 $result $action-runWithParams([id 5]); $result $controller-afterAction($action, $result); }可以认为beforeAction()就是 controller 页面打开前的安检闸门而EVENT_BEFORE_ACTION就是这个闸门上外接的多个电子眼。2. 事件机制的本质从 PHP 数组到触发链2.1 Component::on 与 _events 的存储结构Yii2 的事件机制跟 Node.js 的EventEmitter思路一致只是实现上没那么复杂。它靠每个Component实例上的一个$_events数组来维护事件绑定关系。当你在控制器里写下这段代码$controller-on(Controller::EVENT_BEFORE_ACTION, function ($event) { // do something });实际上是在这个 Controller 实例内部的$_events[beforeAction]数组末尾追加了一个元素。这个元素本身也是一个数组结构大致是$_events [ beforeAction [ [function ($event) { /* handler */ }, null], ], ];第一位是处理器第二位是on()方法传入的$data用于在触发时额外携带一些参数。trigger()的实现里其实也有一个比较微妙的地方。为了防止回调函数执行过程中又动态修改了事件绑定列表导致遍历错乱每次触发时都会先把当前事件名对应的处理器拷贝到一个新数组里再来遍历$eventHandlers []; foreach ($this-_events[$name] as $handler) { $eventHandlers[] $handler; }这个“拷贝”动作保证了事件处理过程中你on()/off()不会影响正在执行的这一轮 handler 列表。2.2 trigger() 的源码级拆解Component::trigger()方法源码整体并不长但可以拆成四个步骤看。public function trigger($name, Event $event null) { // 步骤1确保所有行为都已挂载 $this-ensureBehaviors(); // 步骤2拷贝实例事件处理器列表 $eventHandlers []; foreach ($this-_events[$name] as $handler) { $eventHandlers[] $handler; } // 步骤3补全事件名称和状态 if ($event null) { $event new Event(); } if ($event instanceof Event) { $event-handled false; $event-name $name; } // 步骤4逐个调用支持提前打断 foreach ($eventHandlers as $handler) { $event-data $handler[1]; call_user_func($handler[0], $event); if ($event-handled) { return; } } }先看步骤 1。ensureBehaviors()是所有事件触发之前的保险操作它会让组件先检查并挂载定义在behaviors()方法里的所有行为对象。这样你放在行为里的事件处理器能跟直接在控制器里on()绑定的处理器一起被找到。再看步骤 4 里最容易被忽略的地方$event-data $handler[1]这句代码把绑定时传入的第二个参数重新赋值到了$event-data上。这意味着不管你绑定了多少 handler每次执行时拿到的$event-data都是当前这个 handler 特有的数据不是其他 handler 的数据。这个设计让事件处理器能接收静态参数又不至于污染闭包上下文。$controller-on( Controller::EVENT_BEFORE_ACTION, function ($event) { // 通过 $event-data 取到 inner $environment $event-data; }, inner );步骤 4 中还有一个$event-handled的判断。handled这个属性默认是false只要某个 handler 内部把它改成true后续实例事件全部不会执行了。这是事件分发机制里唯一的一个“硬中断”手段。2.3 类级别事件与实例事件的差别除了在组件实例上调用$controller-on()这种实例事件之外Yii2 还支持一种类级别的静态事件注册Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, function ($event) { // 所有 Controller 实例触发 beforeAction 时都会执行 });在 Controller 的trigger()调用完所有实例处理器之后Component::trigger()的末尾还有一句Event::trigger($this, $name, $event)就是把事件递给类级别的静态事件中心处理。如果你在日常项目里看到全站基类控制器基本上会遇到两种方案传统的做法写一个BaseController继承Controller在init()里$this-on(...)用静态事件直接在某处通常是bootstrap阶段或公共配置里用Event::on()做一个全局监听对所有 Controller 实例生效第二种方案的好处在于不侵入控制器基类新接入的组件可以在不修改既有类继承关系的情况下完成横切逻辑的注入。缺点是这类事件会全局扩散排查问题时路径较长建议只在框架级组件里使用。3. EVENT_BEFORE_ACTION 与 beforeAction() 的恩怨情仇3.1 两个“同名者”的分工很多从 Yii1 转 Yii2 的老 PHP 容易产生一个疑问既然有了beforeAction()方法可以重写为什么还要搞一个EVENT_BEFORE_ACTION事件两个功能不是完全重叠了说实话这个设计是 Yii2 框架将 Yii1 时代的“方法重写型钩子”抽象成“可组合事件”的典型案例。Yii1 时代想做全局权限校验通常得去改基类控制器把所有逻辑写在一个类里很容易变成一个越滚越大的“上帝基类”。Yii2 则把触发动作做成事件让外部行为、扩展包、其他模块都可以在不改动类继承结构的情况下往同一个钩子上挂逻辑。在你写的业务子类里可以重写beforeAction()public function beforeAction($action) { if (!Yii::$app-user-isGuest) { return false; } return parent::beforeAction($action); }如果你要复用父类里的事件系统就必须调用parent::beforeAction($action)。这一点一定要养成习惯——所有 Yii2 的官方示例里重写beforeAction()方法时也都会调用parent::beforeAction($action)。那外部监听呢// 在不改动控制器子类的情况下也可以挂事件 $controller-on(Controller::EVENT_BEFORE_ACTION, function ($event) { // your logic });可以这样理解beforeAction()是“类内部可以重写的钩子方法”它决定 action 是否执行EVENT_BEFORE_ACTION是“让外部代码参与拦截的机会”它通过ActionEvent::isValid影响beforeAction()的返回结果子类重写的beforeAction()是整个触发链条的开端你必须让事件的入口有执行的机会3.2 优先级和顺序的真相有经验的开发者经常会问如果控制器里同时绑定了多个 handler哪个先执行结论是按on()绑定顺序执行后绑定的排在后面。同一个行为里的events()方法在行为挂载时被绑定多个行为的集成顺序也会影响事件执行顺序。on()方法的第四个参数$append才是控制追加顺序的关键public function on($name, $handler, $data null, $append true)如果$append false新 handler 会被插到事件链最前端。这意味着如果你在模块级绑了一个优先级很高的通用事件希望它永远跑在最前面可以写Yii::$app-controller-on( Controller::EVENT_BEFORE_ACTION, [$object, method], null, false );记得一件事Yii2 的事件机制里没有“事件优先级数字”这种东西你想控制的“优先级”本质上是控制“绑定顺序”。这比 thinkphp 或 Laravel 里那种数字优先级更直观也更容易排查——你只要查绑定顺序就能判断结果。3.3 重写前先想清楚要不要 parent新手最常踩的一个坑就是重写beforeAction()时忘了调用parent::beforeAction()导致所有通过事件挂载的监听逻辑失效。网上经常有人反馈说“我的日志行为没生效”“后台权限校验变成摆设”最后发现原因就是这种。一个更稳健的写法是尽量不要重写beforeAction()方法而是改用行为public function behaviors() { return [ accessFilter [ class AccessFilter::class, ], ]; }这一方案的底层逻辑是让 Yii2 的ensureBehaviors()自动管理事件绑定。你业务类里不覆盖父类方法父类通过runAction()正常调用beforeAction()事件就天然会触发自然不会出现漏调parent的问题。如果你确实需要一个便捷的基类钩子推荐把逻辑放在控制器自己的init()或者自定义一个空方法里让这个“自己的钩子”跑在事件之外public function beforeAction($action) { // 自定义的新钩子放这里 $this-customBeforeActionLogic($action); // 事件机制依然走父类 return parent::beforeAction($action); }规则总结成一句代码习惯子类重写时parent::beforeAction($action)这一行必须放在方法返回之前。4. Behavior 在事件中的角色为什么行为里的事件像在控制器里一样4.1 Behavior::attach 的挂载过程你写过一个最简单的行为类吗通常长这样namespace app\behaviors; use yii\base\Behavior; use yii\base\Controller; class LogBehavior extends Behavior { public function events() { return [ Controller::EVENT_BEFORE_ACTION logBeforeAction, ]; } public function logBeforeAction($event) { // ... } }控制器里这样引入public function behaviors() { return [ log LogBehavior::class, ]; }运行时这个事件的绑定路径是怎么完成的不妨追一下行为挂载的过程。当 Controller 第一次调用ensureBehaviors()之后通过createBehaviors()把behaviors()数组里的每个定义实例化为行为对象。随后每个行为对象会执行attach($owner)。Behavior::attach()方法里有一段重要逻辑public function attach($owner) { $this-owner $owner; foreach ($this-events() as $event $handler) { $owner-on($event, is_string($handler) ? [$this, $handler] : $handler); } }它会遍历行为里定义的events()映射然后把每个事件处理函数以[$this, $handler]的形式绑定到当前 owner也就是 Controller的实例事件列表里。这解释了为什么行为里的logBeforeAction($event)方法能拿到$event-action也解释了为什么行为之间可以跟控制器内部on()的处理器“混编”在同一个事件链上。4.2 行为里的 handler 怎么收到 ActionEvent行为的事件处理器和普通匿名函数接收的事件对象没有任何特殊之处。它收到的就是Controller::beforeAction()里创建的那个ActionEvent实例。因此你完全可以在行为里操作isValidpublic function logBeforeAction($event) { if (!$this-isAllowed($event-action)) { $event-isValid false; } }这样可以拦截 action 执行。也可以不拦截只是记录日志。4.3 把行为事件和通用权限过滤结合为了让你直观感受这种属性的便利给你看一个权限过滤行为的经典实现方式可以作为一个代码模板来抄namespace app\behaviors; use yii\base\Behavior; use yii\base\Controller; use yii\base\InvalidConfigException; use Yii; class AccessBehavior extends Behavior { public $actions []; public function events() { return [ Controller::EVENT_BEFORE_ACTION beforeAction, ]; } public function beforeAction($event) { $actionId $event-action-id; $controllerId $event-action-controller-id; if (in_array($controllerId/$actionId, $this-actions, true)) { if (Yii::$app-user-isGuest) { $event-isValid false; Yii::$app-response-redirect([site/login]); } } } }然后绑定到指定的控制器类里即可。对一个项目来说这种写法比在每个 Controller 里重写beforeAction要优雅得多。4.4 给行为里的 handler 传参数注意Behavior::events()里的 handler 字符串必须是行为类内部真实存在的方法。但 events() 方法本身返回的数据映射无法放置额外参数如果你想传参就在方法内部使用行为属性。也就是说在行为类里声明需要的属性然后在控制器定义行为时给属性赋值public function behaviors() { return [ access [ class AccessBehavior::class, actions [admin/delete], ], ]; }行为本质上就是一段可复用的事件绑定逻辑加一些业务属性灵活程度很高也是 Yii2 中最推崇的一种代码组合复用方式。5. ActionEvent 的字段能做什么ActionEvent是所有和 action 执行相关事件的事件类基类。在Controller::beforeAction()中传入的ActionEvent对象里有三个核心属性需要区分清楚。属性默认值作用$action当前 Action 对象处理器可以通过它拿到 actionId、controllerId 等信息$isValidtrue在 beforeAction 阶段被设为 false 可以终止后续 action 执行$handledfalse在事件链中把该属性设为 true 会中断同一事件后续 handler 的继续触发我建议你把这个记在心里handled影响的是“事件传播”本身isValid影响的则是“Action 要不要被执行”。两者一点都不能混。5.1 isValid 如何阻止 Action 执行当你在事件里把isValid置为 falsepublic function beforeAction($event) { $event-isValid false; }框架里的判断流程会变成$event new ActionEvent($action); $this-trigger(self::EVENT_BEFORE_ACTION, $event); return $event-isValid; // false此时 Controller 的runAction()收到beforeAction()的返回值false就不会继续执行$action-runWithParams()进而整个 action 的结果为 null。这个特性非常适合在系统维护或接口禁用时做统一拦截。5.2 如何在 handler 中优雅拿到当前请求信息很多初学者会遇到一个问题事件回调里拿到$event-action之后想知道当前请求对应的路由怎么那么绕实际上你不需要从 action 里挖到底直接由 Yii2 提供的全局请求变量就能拿全public function beforeAction($event) { $route Yii::$app-requestedRoute; $actionId $event-action-id; $controllerId $event-action-controller-id; // 有的回调里会有模块前缀需要拼接 }习惯上如果能拿$event-action-uniqueId就是带模块名的完整 ID。$this-action-uniqueId // module/controller/action这个 uniqueId 在前后端分离的 API 网关里写日志时会非常顺手。5.3 handler 中被改动过的 resultActionEvent 的$result属性是在EVENT_AFTER_ACTION阶段使用的但在EVENT_BEFORE_ACTION阶段它默认是 null。如果在 before 事件里给$result赋了值当 action 正常执行后afterAction()阶段也会持有原 action 的返回值而不是 before 阶段赋值的内容。不要试图在EVENT_BEFORE_ACTION阶段用$result来替换 action 返回结果这不是它的工作方式。想在 action 执行前直接返回响应标准的方案是手动给response赋值并终止后续执行public function beforeAction($event) { // 某些条件下直接返回自定义 JSON 响应 if ($this-checkFailed()) { Yii::$app-response-statusCode 400; Yii::$app-response-data [code 400, msg bad request]; $event-isValid false; } }但注意上面的 response 只是赋值框架还是会继续执行后续路由处理。你需要确保设置了isValid falseController 的runAction()才知道不用继续跑 action 了。6. 真实场景中的实战配置与实现6.1 一个“全局接口日志”事件方案假设我们前后端分离的 API 项目要求把所有请求的 method、route、status、耗时都记录到日志表中。用事件来做之前先把什么信息能拿到列出来再定义行为类namespace app\behaviors; use Yii; use yii\base\Behavior; use yii\base\Controller; class ApiLogBehavior extends Behavior { public $excludeRoutes []; public function events() { return [ Controller::EVENT_BEFORE_ACTION logBegin, Controller::EVENT_AFTER_ACTION logEnd, ]; } public function logBegin($event) { $route $event-action-uniqueId; if (in_array($route, $this-excludeRoutes, true)) { return; } Yii::$app-params[request_begin_time] microtime(true); Yii::$app-params[request_begin_route] $route; Yii::$app-params[request_begin_method] Yii::$app-request-method; } public function logEnd($event) { $beginTime Yii::$app-params[request_begin_time] ?? null; if ($beginTime null) { return; } $duration microtime(true) - $beginTime; \Yii::info([ route Yii::$app-params[request_begin_route], method Yii::$app-params[request_begin_method], status Yii::$app-response-statusCode, duration round($duration * 1000, 2) . ms, ], api_log); } }把行为挂到应用组件里// 某个配置文件中 web [ as apiLog [ class app\\behaviors\\ApiLogBehavior, excludeRoutes [debug/default/index], ], ]这样每个 controller 执行前后都会自动调用行为里的两个方法日志逻辑和业务完全解耦。6.2 对整类控制器做权限拦截的三种姿势场景想把所有backend模块下的控制器做一次登录校验。姿势一直接对当前控制器基类重写beforeAction()。这是传统方法简单但侵入性强。姿势二通过Event::on()注册类级别的静态事件针对整个 Controller 的派生类生效Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, function ($event) { if (Yii::$app-user-isGuest) { $event-isValid false; } });这种写法的缺点是它会拦截所有 Controller包括 frontend、api如果想只过滤某个模块还需要在回调里加模块判断。写法上灵活性足够但要注意影响面。姿势三写一个专门的行为类在目标模块的所有控制器里统一挂载。这算是最规范的方式把一个行为定义到该模块的main.php中并在 base controller 的behaviors()里 return。6.3 判断维护状态在事件里建议怎么写最常见的真实案例就是后台管理员开启系统维护模式后所有前端访问跳转到一个固定页面。你可以在全局站点入口或主控制器上监听EVENT_BEFORE_ACTIONYii::$app-on(Controller::EVENT_BEFORE_ACTION, function ($event) { if (Yii::$app-params[maintenanceMode] true) { $event-isValid false; Yii::$app-response-content file_get_contents( Yii::getAlias(webroot) . /maintenance.html ); } });需要注意它不是万能的。如果写在 Application 上只会对控制器触发但并不是对所有路径生效。6.4 如何动态向已有组件追加事件Yii2 里很多组件本身也是Component因此你可以轻松对其追加自己的监听逻辑。比如以下载 Excel 为例在 Controller 中出现一个下载文件名的处理每次都会在下载前拦截住并改变文件名。public function init() { parent::init(); $this-on(Controller::EVENT_BEFORE_ACTION, [$this, changeDownloadName]); }像这样把事件绑定写在init()里对当前 Controller 实例是安全性最高的做法但不建议做全局操作。因为init()只针对当前类当前实例。7. 踩坑实录与排查建议7.1 事件未触发的优先排查路径我遇到过好多次刚用事件时“明明绑了怎么没效果”。建议按以下顺序检查是否重写了beforeAction()又没调用parent::beforeAction($action)。这个原因占比能到一半。是否在behaviors()里写错了类名导致行为没有被ensureBehaviors()加载。可以通过打印$controller-getBehaviors()检查。绑定时机是否太晚。比如你是在EVENT_BEFORE_ACTION的处理器内部再去绑定另一个EVENT_BEFORE_ACTION那不生效是必然的——当前事件已经分发到一半了。这一点要明确在事件执行过程中添加的新 handler 不会在下一次触发前生效。是否在模块路由解析阶段就挂了请求。如果路由根本没指向目标 Controller那 Controller 的事件确实不会触发。7.2 在事件中断言或抛异常需要谨慎虽然技术上允许你写throw new \Exception(deny)来粗暴拦截但要考虑错误处理机制是否兜得住。建议如果是面向外部 API统一用 HTTP 状态码加$event-isValid false的组合方式拦截在框架层统一做针对性的响应格式转换。7.3 多个行为挂在同一个控制器时的事件顺序排查多个行为可能都属于EVENT_BEFORE_ACTION事件此时执行顺序和behaviors()里声明的顺序一致。但如果你在子类里定义behaviors()时还用了父类的行为合并操作public function behaviors() { return array_merge(parent::behaviors(), [ extraBehavior [...] ]); }父类行为先绑定、先执行子类行为后绑定、后执行。若是反过来用array_merge($extraBehaviors, parent::behaviors())顺序就完全相反。把这个当成一把很实在的控制棒可以优先决定哪个权限校验先跑。7.4 用 Yii debug 面板检查事件是否真的被查到如果你用 Yii2 debug 面板可以在 Log 分类中找yii\base\Event有关项。但多数情况下更高效的方式是在事件处理器里直接打一个日志点Yii::debug(execute access behavior, __METHOD__);当所有可排查的手段都用尽后回到源码就最可靠。把Controller::beforeAction()、Component::trigger()、Behavior::attach()三个方法串起来看一遍思路会很清朗。最后再分享一个小技巧如果你在多个项目里复用同一个行为类建议在events()方法里不要直接写beforeAction这种裸字符串而是写成Controller::EVENT_BEFORE_ACTION。原因是 IDE 补全和常量引用的可维护性更好框架升级时容错也更高。我和身边同事做代码 review 时看到硬编码字符串的事件名都会提醒改掉这不是洁癖是实打实能减少线上事故的习惯。回到开头那个权限失效的问题——当时就是因为 Controller 重写了beforeAction()并且在里面做自定义权限校验后直接return true没有调用父类方法事件从头到尾没被触发。这个坑掉过一次之后我给自己立了一条规矩只要动了beforeAction()第一行就是parent::beforeAction($action)即使暂时用不到也要保证事件链路不断。Yii2 的事件机制经得起反复读源码理解透一层之后再看行为、模块、控制器的协作方式会顺很多。你与其在搜索引擎里翻“EVENT_BEFORE_ACTION 不生效”的帖子不如花半小时把Component.php里的trigger()彻底看一遍。框架这件事还是源码最诚实。
返回列表