Yii 2 事件机制完全指南:基于 yii\base\Component 的事件绑定、触发与高级应用
【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2
事件(Event)是 Yii 2 框架中实现“控制反转”式代码注入的核心机制:它允许你在既有代码的特定执行点插入自定义逻辑,而无需修改原有类的实现。本指南以官方文档 docs/guide-ja/concept-events.md 为主线,结合框架源码 framework/base/Component.php 与 framework/base/Event.php 的底层实现,系统讲解事件处理器(Event Handler)的四种回调形式、实例级与类级事件的绑定/触发/解绑、接口级事件、全局事件以及 2.0.14 引入的通配符事件。读完本文,你将能够在自己的组件、Active Record 模型和应用生命周期中熟练运用 Yii 2 事件体系,并理解其内部执行顺序与性能权衡。
一、事件机制概览:向既有代码注入自定义逻辑
事件机制解决的核心问题是:如何在不改动既有类的前提下,在特定执行点挂接自定义行为。以官方文档的邮件发送场景为例:一个 mailer 对象在成功发送消息后会触发messageSent事件;如果希望追踪所有成功发送的邮件,只需把追踪代码附加到messageSent事件上即可,完全不需要修改 mailer 的发送逻辑。
在 Yii 2 中,事件支持由基类 yii\base\Component 提供。任何需要触发事件的类都必须继承yii\base\Component或其子类——例如yii\db\ActiveRecord、yii\web\Controller、应用主体Application都间接继承自它。Component在父类 yii\base\BaseObject 的属性(Property)功能基础上,叠加了事件(Event)与行为(Behavior)两大特性,其类注释对此有明确说明。
从源码看,实例级事件依赖两个私有数组存储:$_events(事件名 => 处理器列表)与$_eventWildcards(通配符模式 => 处理器列表,自 2.0.14 起引入)。所有事件操作的入口均可归结为三个实例方法on()、off()、trigger()和三个对应静态方法Event::on()、Event::off()、Event::trigger()。
二、事件处理器(Event Handler):四种 PHP 回调形式
事件处理器本质上是一个 PHP 回调(Callable),在它所绑定的事件被触发时执行。官方文档明确列出了四种可用形式:
- 全局函数:以字符串指定函数名(不含括号),例如
'trim'; - 对象方法:以
[$object, 'methodName']数组指定; - 静态类方法:以
['ClassName', 'methodName']数组指定; - 匿名函数:例如
function ($event) { ... }。
处理器签名为function ($event) { ... },其中$event是 yii\base\Event 或其子类的实例。通过$event参数,处理器可以获得三类关键信息:
| 属性 | 含义 |
|---|---|
$event->name | 事件名称 |
$event->sender | 事件发送者,即调用trigger()方法的对象 |
$event->data | 附加数据,即绑定处理器时通过on()第三参数传入的数据 |
Event类的四个公共属性在源码 framework/base/Event.php#L32-L54 中定义,其中handled属性默认false,用于控制事件链的中断(见下文)。
三、绑定事件处理器:on() 与配置式绑定
3.1 通过 Component::on() 绑定
调用 Component::on() 即可把处理器附加到实例事件上。官方文档给出的四种绑定示例:
$foo = new Foo(); // 处理器是全局函数 $foo->on(Foo::EVENT_HELLO, 'function_name'); // 处理器是对象方法 $foo->on(Foo::EVENT_HELLO, [$object, 'methodName']); // 处理器是静态类方法 $foo->on(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']); // 处理器是匿名函数 $foo->on(Foo::EVENT_HELLO, function ($event) { // 事件处理逻辑 });从源码看,on()的内部实现非常直接:若事件名含*,则存入$_eventWildcards,否则存入$_events[$name],每个条目为[$handler, $data]二元组。
3.2 通过配置数组绑定(on 前缀语法)
除了编程式调用,还可以在组件配置数组中直接声明事件处理器。官方文档(参见 概念:配置 的“配置格式”一节)给出的语法如下:
[ 'on eventName' => $eventHandler, ]例如 docs/guide/concept-configurations.md#L70 中的实际示例:
'on search' => function ($event) { // 搜索事件处理逻辑 },在应用配置(如config/main.php的components段)中,可以这样为某个组件声明事件:
'components' => [ 'mailer' => [ 'class' => 'app\components\Mailer', 'on messageSent' => function ($event) { Yii::info('邮件已发送: ' . $event->data); }, ], ],Component类注释 framework/base/Component.php#L54-L63 明确给出了'on add' => function ($event) { ... }的配置格式,其解析逻辑位于BaseObject::__construct()中:配置键以on开头时,会调用$this->on($name, $handler)完成绑定。这种声明式写法非常适合在入口脚本或应用配置中统一挂接横切逻辑。
3.3 传递附加数据(第三参数)
绑定处理器时,可通过on()的第三参数携带自定义数据,事件触发时数据会出现在$event->data中。官方文档示例:
// 事件触发时会输出 "abc" // 因为 $event->data 保存了传给 "on" 的第三个参数 $foo->on(Foo::EVENT_HELLO, 'function_name', 'abc'); function function_name($event) { echo $event->data; }在 Component::trigger() 的调用循环中可以看到,每个处理器执行前都会执行$event->data = $handler[1];,即把该处理器绑定时的数据注入$event->data。这意味着不同处理器可以携带各自不同的附加数据,互不干扰。
四、处理器的执行顺序与中断控制
一个事件可以绑定多个处理器。事件触发时,处理器按照绑定顺序依次调用。Component::trigger()内部通过array_merge合并通配符与精确事件处理器后逐一遍历执行(framework/base/Component.php#L647-L654)。
4.1 通过 $event->handled 中断后续处理器
如果某个处理器需要阻止后续处理器执行,将$event->handled设为true即可:
$foo->on(Foo::EVENT_HELLO, function ($event) { $event->handled = true; });源码中handled的语义是“事件是否已被处理”:一旦处理器将其置为true,trigger()立即return,跳过剩余处理器。测试 tests/framework/base/EventTest.php 中的testTriggerWithHandledEvent专门验证了这一点。
4.2 通过第四参数 $append 插入队首
默认情况下,新绑定的处理器追加到事件处理器队列末尾,因此最后被调用。若希望新处理器最先被调用,将第四参数$append设为false:
$foo->on(Foo::EVENT_HELLO, function ($event) { // ... }, $data, false);源码对应逻辑为:$append为true时执行$this->_events[$name][] = [$handler, $data];,为false时执行array_unshift(...)插入队首(framework/base/Component.php#L547-L551)。测试testOnPrependPlainHandler与testOnPrependWildcardHandler(tests/framework/base/ComponentTest.php#L529-L558)覆盖了普通与通配符两种场景的插队行为。
五、触发事件:trigger() 与事件对象
5.1 最小触发示例
事件通过调用 Component::trigger() 触发,该方法必须传入事件名,第二个参数可选地传入事件对象:
namespace app\components; use yii\base\Component; use yii\base\Event; class Foo extends Component { const EVENT_HELLO = 'hello'; public function bar() { $this->trigger(self::EVENT_HELLO); } }以上代码中,每次调用bar()都会触发名为hello的事件。文档特别建议:使用类常量表示事件名(如EVENT_HELLO),此举有三个好处——防止拼写错误、让 IDE 自动补全可识别事件、通过查看常量声明即可了解类支持哪些事件。
5.2 携带自定义事件对象
当需要向处理器传递额外信息时,可以创建Event子类,并通过trigger()第二参数传入。官方文档的 Mailer 示例:
namespace app\components; use yii\base\Component; use yii\base\Event; class MessageEvent extends Event { public $message; } class Mailer extends Component { const EVENT_MESSAGE_SENT = 'messageSent'; public function send($message) { // ...发送 $message... $event = new MessageEvent; $event->message = $message; $this->trigger(self::EVENT_MESSAGE_SENT, $event); } }处理器端即可通过$event->message访问具体消息内容。从 Event 源码看,事件对象必须为yii\base\Event或其子类实例;若trigger()未传事件对象,内部会自动new Event()。
5.3 触发时的内部流程
综合源码 framework/base/Component.php#L623-L659 与 Event::trigger(),一次事件触发经历如下流程:
trigger($name, $event)先收集当前组件实例上匹配的通配符处理器与精确名称处理器;- 若
$event为null,创建默认Event对象; - 若
$event->sender为null,自动设为当前组件($this); - 设置
$event->name = $name、$event->handled = false,随后按序调用实例级处理器; - 实例级处理器执行完毕后,调用
Event::trigger($this, $name, $event)接力触发类级处理器——即“先实例级、后类级”的执行顺序(与文档描述一致)。
六、解绑处理器:off()
6.1 解绑单个处理器
调用 Component::off() 可从事件上移除指定处理器:
// 处理器是全局函数 $foo->off(Foo::EVENT_HELLO, 'function_name'); // 处理器是对象方法 $foo->off(Foo::EVENT_HELLO, [$object, 'methodName']); // 处理器是静态类方法 $foo->off(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']); // 处理器是匿名函数 $foo->off(Foo::EVENT_HELLO, $anonymousFunction);匿名函数需要特别注意:除非绑定匿名函数时已把它保存到变量(如上述$anonymousFunction),否则无法将其作为解绑参数定位到同一回调。源码通过$event[0] === $handler做严格全等比较(framework/base/Component.php#L583),匿名函数若不保存引用将无法匹配。
6.2 解绑全部处理器
省略第二参数即可移除某事件上的全部处理器:
$foo->off(Foo::EVENT_HELLO);对应源码逻辑为unset($this->_events[$name], $this->_eventWildcards[$name]); return true;(framework/base/Component.php#L574-L577)。若指定事件名不存在任何处理器,off()返回false。
七、类级事件处理器:Event::on() / Event::trigger() / Event::off()
7.1 监听所有实例的同一事件
前面讨论的都是实例级绑定。如果希望响应某个类的所有实例(包括子类)触发的事件,则应使用静态方法 Event::on()。官方文档的经典场景是追踪所有 Active Record 插入操作:
use Yii; use yii\base\Event; use yii\db\ActiveRecord; Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) { Yii::debug(get_class($event->sender) . ' が挿入されました'); // 记录被插入的类 });此后,任何ActiveRecord或其子类实例触发EVENT_AFTER_INSERT时,该处理器都会被调用,并通过$event->sender获取触发事件的具体对象。Event::trigger()在源码中会合并class_parents($class)与class_implements($class)逐级查找处理器(framework/base/Event.php#L292-L296),这正是“父类/接口上注册的类级处理器也能被调用”的原因。
7.2 触发类级事件
通过 Event::trigger() 可触发类级事件。类级事件不关联具体对象,因此只会调用类级处理器,且$event->sender为null:
use yii\base\Event; Event::on(Foo::class, Foo::EVENT_HELLO, function ($event) { var_dump($event->sender); // 输出 "null" }); Event::trigger(Foo::class, Foo::EVENT_HELLO);7.3 类级解绑与使用注意
类级解绑同样使用静态方法:
// 解绑 $handler Event::off(Foo::class, Foo::EVENT_HELLO, $handler); // 解绑 Foo::EVENT_HELLO 的全部处理器 Event::off(Foo::class, Foo::EVENT_HELLO);使用警告:类级处理器会响应该类及其所有子类实例触发的事件,因此务必谨慎,尤其是当目标类是低层基类时(如yii\base\BaseObject)——稍有不慎就会捕获到框架内部大量对象的同名事件,造成难以排查的副作用。
八、接口级事件:用接口抽象事件契约
事件机制还支持更抽象的接口级用法:为特定事件定义一个接口,让需要触发该事件的类实现它。官方文档的示例:
namespace app\interfaces; interface DanceEventInterface { const EVENT_DANCE = 'dance'; }两个实现类:
class Dog extends Component implements DanceEventInterface { public function meetBuddy() { echo "ワン!"; // 汪! $this->trigger(DanceEventInterface::EVENT_DANCE); } } class Developer extends Component implements DanceEventInterface { public function testsPassed() { echo "よっしゃ!"; // 太好了! $this->trigger(DanceEventInterface::EVENT_DANCE); } }要统一处理任意实现类触发的EVENT_DANCE,把接口类名作为Event::on()的第一参数:
Event::on('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) { Yii::debug(get_class($event->sender) . ' が躍り上がって喜んだ。'); // 记录是狗还是开发者触发的 });也可分别触发具体类的该事件:
// 触发 Dog 类的事件 Event::trigger(Dog::class, DanceEventInterface::EVENT_DANCE); // 触发 Developer 类的事件 Event::trigger(Developer::class, DanceEventInterface::EVENT_DANCE);重要限制:不能通过接口名一次性触发所有实现类的事件:
// 无效!不会触发实现该接口的类的事件 Event::trigger('app\interfaces\DanceEventInterface', DanceEventInterface::EVENT_DANCE);接口级解绑方式与类级一致,第一参数传接口类名即可。这一机制之所以可行,是因为Event::trigger()内部通过class_implements($class, true)把接口纳入查找链(framework/base/Event.php#L292-L296)。
九、全局事件:借助应用单例的分布式事件
Yii 2 支持所谓的全局事件,本质上是基于上述事件机制的巧妙运用:需要一个全局可访问的单例(如应用实例Yii::$app)作为中转站。事件发送者不调用自身的trigger(),而是调用单例的trigger();处理器同样绑定到单例上。官方文档示例:
use Yii; use yii\base\Event; use app\components\Foo; Yii::$app->on('bar', function ($event) { echo get_class($event->sender); // 输出 "app\components\Foo" }); Yii::$app->trigger('bar', new Event(['sender' => new Foo]));全局事件的优势在于:绑定处理器时无需持有未来触发事件的对象——绑定与触发都通过单例完成,实现了发送方与监听方的彻底解耦。这在模块间通信、应用级钩子等场景非常实用。
命名规范提醒:由于全局事件命名空间被所有代码共享,应使用带命名空间的聪明命名避免冲突,例如"frontend.mail.sent"、"backend.mail.sent"这类前缀风格。关于应用实例的更多信息,可参考 应用结构。
十、通配符事件:一次绑定,匹配多个事件
自Yii 2.0.14起,事件名与类名均支持通配符模式,*可匹配任意字符。
10.1 实例级通配符
use Yii; $foo = new Foo(); $foo->on('foo.event.*', function ($event) { // 对任何以 'foo.event.' 开头的事件触发 Yii::debug('trigger event: ' . $event->name); });10.2 类级通配符
通配符同样适用于类名与事件名:
use yii\base\Event; use Yii; Event::on('app\models\*', 'before*', function ($event) { // 对命名空间 'app\models' 下所有类、且名称以 'before' 开头的所有事件触发 Yii::debug('trigger event: ' . $event->name . ' for class: ' . get_class($event->sender)); });甚至可以捕获所有类、所有事件:
use yii\base\Event; use Yii; Event::on('*', '*', function ($event) { // 对任何类的任何事件触发 Yii::debug('trigger event: ' . $event->name); });10.3 实现与性能警告
通配符匹配由StringHelper::matchWildcard()实现(Component::trigger()与Event::trigger()中均会调用)。匹配过程需要对每个注册的通配符模式做一次匹配运算,文档与源码都明确指出:使用通配符设置事件处理器可能降低应用性能,应尽量避免。
10.4 通配符解绑的特殊语义
解绑通配符处理器时,必须重复使用相同的通配符模式。且注意:off()传入通配符只会解绑通过该通配符注册的处理器,普通事件名注册的处理器即使恰好匹配该模式也不会被移除。官方文档示例:
use Yii; $foo = new Foo(); // 绑定普通处理器 $foo->on('event.hello', function ($event) { echo 'direct-handler'; }); // 绑定通配符处理器 $foo->on('*', function ($event) { echo 'wildcard-handler'; }); // 只解绑通配符处理器! $foo->off('*'); $foo->trigger('event.hello'); // 输出: 'direct-handler'此语义在Component::off()(framework/base/Component.php#L568-L612)与Event::off()(framework/base/Event.php#L139-L188)中均有明确实现与注释。测试 tests/framework/base/ComponentTest.php#L246-L306 的testOnWildcard、testOffWildcard、testTriggerWildcard以及 tests/framework/base/EventTest.php 中多个通配符相关用例,系统验证了绑定、解绑、hasHandlers()判定等行为。
十一、框架内的典型事件应用
11.1 Active Record 生命周期事件
Active Record 在增删改查的关键节点触发事件,如EVENT_BEFORE_INSERT、EVENT_AFTER_INSERT、EVENT_BEFORE_UPDATE、EVENT_AFTER_UPDATE、EVENT_BEFORE_DELETE、EVENT_AFTER_DELETE等(定义于 yii\db\BaseActiveRecord)。利用类级绑定即可全局拦截数据操作,例如审计日志、缓存失效等场景。相关基础概念可参考 Active Record 指南。
11.2 应用级事件
应用对象(Yii::$app)本身也是Component,其引导(Bootstrap)与请求处理流程中触发多个事件,例如Application::EVENT_BEFORE_REQUEST、EVENT_AFTER_REQUEST等。这些事件常用于挂接登录校验、性能监控、统一响应处理等横切逻辑,可参考 应用结构 与 应用组件。
11.3 与行为(Behavior)的关系
事件与行为紧密关联:行为本质上是通过事件机制附加到组件上的(ensureBehaviors()在每个事件方法开头被调用,见 framework/base/Component.php#L536)。理解事件机制是掌握行为机制的基础。
十二、总结与实践建议
| 维度 | 关键结论 |
|---|---|
| 事件基类 | yii\base\Component(需继承才能触发事件) |
| 事件对象 | yii\base\Event及其子类,含name/sender/data/handled |
| 绑定 | 实例级Component::on();类级Event::on();配置式'on eventName' => handler |
| 触发 | 实例级Component::trigger();类级Event::trigger();全局事件借道Yii::$app |
| 解绑 | Component::off()/Event::off(),通配符解绑需重复相同模式 |
| 执行顺序 | 实例级先于类级;同级按绑定顺序;$handled = true可中断;$append = false可插队 |
| 性能 | 通配符模式会增加匹配开销,谨慎使用 |
实践建议:优先使用类常量定义事件名;匿名函数处理器如需解绑请保存变量引用;类级与通配符处理器属于“高影响力”的全局钩子,务必在应用中明确登记并控制其数量;在性能敏感的路径上避免'*'级别的全量通配。掌握了这套事件机制,你便能在不侵入既有代码的前提下,灵活地为组件、数据模型与应用生命周期挂接各类扩展逻辑——这也是 Yii 2 高度可扩展、可插拔架构的基石之一。
【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考