ARTICLE DETAIL

资讯详情

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

Yii 2 事件机制完全指南:基于 yii\base\Component 的事件绑定、触发与高级应用

Yii 2 事件机制完全指南:基于 yii\base\Component 的事件绑定、触发与高级应用 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的语义是“事件是否已被处理”一旦处理器将其置为truetrigger()立即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与testOnPrependWildcardHandlertests/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为nulluse 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),仅供参考
返回列表