Yii2 事件监听使用详解

Yii2 事件监听使用详解

一、Yii2 事件系统概述

Yii2 的事件系统基于观察者模式,允许你在特定时刻执行自定义代码。主要组件包括:

  1. 事件 (Event) - 触发动作的信号
  2. 事件处理器 (Event Handler) - 响应事件的代码
  3. 事件监听器 (Event Listener) - 将处理器附加到事件的机制

二、基本使用案例

案例1:类级别事件

// 自定义事件类
namespace app\events;

use yii\base\Event;

class UserSignupEvent extends Event
{
    public $user;
    public $timestamp;
    public $ip;
}
// 触发事件的类
namespace app\models;

use yii\base\Component;
use app\events\UserSignupEvent;

class User extends Component
{
    const EVENT_AFTER_SIGNUP = 'afterSignup';
    
    public function signup()
    {
        // 注册逻辑...
        $this->username = 'john_doe';
        $this->save();
        
        // 触发事件
        $event = new UserSignupEvent();
        $event->user = $this;
        $event->timestamp = time();
        $event->ip = Yii::$app->request->userIP;
        
        $this->trigger(self::EVENT_AFTER_SIGNUP, $event);
    }
}
// 监听器配置 - 在配置文件中
return [
    // ...
    'components' => [
        'user' => [
            'class' => 'app\models\User',
            'on afterSignup' => function ($event) {
                // 发送欢迎邮件
                Yii::$app->mailer->compose()
                    ->setTo($event->user->email)
                    ->setSubject('Welcome!')
                    ->send();
                
                // 记录日志
                Yii::info("User {$event->user->id} signed up from IP: {$event->ip}");
            },
        ],
    ],
];

案例2:全局事件(使用事件触发器)

// 在 bootstrap 或配置文件中绑定全局事件
Yii::$app->on('app.start', function ($event) {
    echo "Application started!";
});

// 在适当的地方触发全局事件
Yii::$app->trigger('app.start');

三、多种监听方式

方式1:匿名函数

$user = new User();

// 绑定匿名函数
$user->on(User::EVENT_AFTER_SIGNUP, function ($event) {
    // 事件处理逻辑
    echo "User {$event->user->username} signed up!";
});

// 触发事件
$user->signup();

方式2:对象方法

class SignupNotifier
{
    public function sendWelcomeEmail($event)
    {
        $user = $event->user;
        // 发送邮件逻辑
        echo "Sending welcome email to {$user->email}";
    }
    
    public function logSignup($event)
    {
        Yii::info("New user: {$event->user->username}");
    }
}

// 使用对象方法作为处理器
$notifier = new SignupNotifier();
$user->on(User::EVENT_AFTER_SIGNUP, [$notifier, 'sendWelcomeEmail']);
$user->on(User::EVENT_AFTER_SIGNUP, [$notifier, 'logSignup']);

方式3:静态类方法

class SignupHandler
{
    public static function handleSignup($event)
    {
        // 静态方法处理事件
        $user = $event->user;
        // 处理逻辑
    }
}

// 绑定静态方法
$user->on(User::EVENT_AFTER_SIGNUP, ['app\handlers\SignupHandler', 'handleSignup']);

方式4:配置文件中配置

// config/web.php
return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'on afterOpen' => function ($event) {
                // 数据库连接后执行
                $event->sender->createCommand("SET time_zone = '+08:00'")->execute();
            },
        ],
        
        'response' => [
            'class' => 'yii\web\Response',
            'on beforeSend' => function ($event) {
                $response = $event->sender;
                // 统一格式化响应
                if ($response->format === 'json') {
                    $response->data = [
                        'success' => $response->statusCode === 200,
                        'data' => $response->data,
                    ];
                }
            },
        ],
    ],
    
    'on beforeRequest' => function ($event) {
        // 应用请求前处理
        Yii::info("Request started: " . Yii::$app->request->url);
    },
];

四、高级用法案例

案例1:模型事件监听

// 在 ActiveRecord 模型中监听事件
namespace app\models;

use yii\db\ActiveRecord;

class Product extends ActiveRecord
{
    public function init()
    {
        parent::init();
        
        // 监听 ActiveRecord 内置事件
        $this->on(self::EVENT_BEFORE_INSERT, [$this, 'beforeInsertHandler']);
        $this->on(self::EVENT_AFTER_UPDATE, [$this, 'afterUpdateHandler']);
        $this->on(self::EVENT_BEFORE_DELETE, [$this, 'beforeDeleteHandler']);
    }
    
    public function beforeInsertHandler($event)
    {
        // 插入前设置默认值
        if (empty($this->created_at)) {
            $this->created_at = time();
        }
        $this->updated_at = time();
    }
    
    public function afterUpdateHandler($event)
    {
        // 更新后记录变更日志
        $changedAttributes = $event->changedAttributes;
        if (!empty($changedAttributes)) {
            $log = new ProductChangeLog();
            $log->product_id = $this->id;
            $log->changes = json_encode($changedAttributes);
            $log->save();
        }
    }
    
    public function beforeDeleteHandler($event)
    {
        // 删除前检查依赖
        if ($this->hasOrders()) {
            Yii::$app->session->setFlash('error', 'Cannot delete product with existing orders');
            $event->isValid = false; // 阻止删除
        }
    }
    
    public function behaviors()
    {
        return [
            [
                'class' => 'yii\behaviors\TimestampBehavior',
                'attributes' => [
                    ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
                ],
            ],
        ];
    }
}

案例2:控制器事件监听

namespace app\controllers;

use yii\web\Controller;
use yii\filters\AccessControl;

class SiteController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::class,
                'rules' => [
                    ['allow' => true, 'roles' => ['@']],
                ],
            ],
        ];
    }
    
    public function init()
    {
        parent::init();
        
        // 监听控制器事件
        $this->on(self::EVENT_BEFORE_ACTION, [$this, 'beforeActionHandler']);
        $this->on(self::EVENT_AFTER_ACTION, [$this, 'afterActionHandler']);
    }
    
    public function beforeActionHandler($event)
    {
        // 记录用户访问日志
        $action = $event->action->id;
        $controller = $event->action->controller->id;
        
        Yii::info("User " . Yii::$app->user->id . 
                 " accessing $controller/$action");
        
        // 可以修改或阻止动作执行
        // $event->isValid = false; // 阻止动作
    }
    
    public function afterActionHandler($event)
    {
        // 动作执行后的处理
        $action = $event->action;
        $result = $event->result;
        
        // 记录执行时间等
    }
}

案例3:模块级别事件

namespace app\modules\admin;

class AdminModule extends \yii\base\Module
{
    public function init()
    {
        parent::init();
        
        // 模块初始化时绑定事件
        Yii::$app->user->on(\yii\web\User::EVENT_AFTER_LOGIN, function ($event) {
            // 管理员登录后记录
            if ($event->identity->isAdmin()) {
                AdminLog::log('login', Yii::$app->request->userIP);
            }
        });
        
        // 监听模块内控制器事件
        $this->on('controller.init', function ($event) {
            // 所有Admin模块控制器初始化时的处理
        });
    }
}

五、事件处理器优先级

// 设置处理器执行顺序(优先级)
$user->on(User::EVENT_AFTER_SIGNUP, 
    [$notifier, 'sendWelcomeEmail'], 
    ['priority' => 10]  // 优先级越高越先执行
);

$user->on(User::EVENT_AFTER_SIGNUP, 
    [$notifier, 'logSignup'], 
    ['priority' => 5]  // 后执行
);

// 也可以使用 prepend 方法将处理器添加到队列开头
$user->on(User::EVENT_AFTER_SIGNUP, 
    function ($event) {
        echo "This handler will execute first!";
    },
    false  // false 表示添加到开头
);

六、事件解绑和通配符

// 1. 解绑特定处理器
$user->off(User::EVENT_AFTER_SIGNUP, [$notifier, 'sendWelcomeEmail']);

// 2. 解绑所有处理器
$user->off(User::EVENT_AFTER_SIGNUP);

// 3. 解绑对象的所有事件处理器
$user->off();

// 4. 通配符事件(需要自定义实现)
class EventManager extends Component
{
    public function on($name, $handler, $data = null, $append = true)
    {
        // 支持通配符,如 'user.*'
        parent::on($name, $handler, $data, $append);
    }
    
    public function trigger($name, Event $event = null)
    {
        // 触发匹配通配符的事件
        $pattern = str_replace('*', '.*', $name);
        foreach ($this->_events as $eventName => $handlers) {
            if (preg_match("#^{$pattern}$#", $eventName)) {
                parent::trigger($eventName, $event);
            }
        }
    }
}

七、实际应用案例:用户注册完整流程

// 1. 定义多个事件类
namespace app\events;

use yii\base\Event;

class UserBeforeRegisterEvent extends Event
{
    public $user;
    public $isValid = true;
    public $validationErrors = [];
}

class UserAfterRegisterEvent extends Event
{
    public $user;
    public $activationToken;
}
// 2. 用户服务类
namespace app\services;

use Yii;
use app\models\User;
use app\events\UserBeforeRegisterEvent;
use app\events\UserAfterRegisterEvent;

class UserService extends \yii\base\Component
{
    const EVENT_BEFORE_REGISTER = 'beforeRegister';
    const EVENT_AFTER_REGISTER = 'afterRegister';
    
    public function register($data)
    {
        $user = new User();
        $user->load($data, '');
        
        // 触发注册前事件
        $beforeEvent = new UserBeforeRegisterEvent(['user' => $user]);
        $this->trigger(self::EVENT_BEFORE_REGISTER, $beforeEvent);
        
        if (!$beforeEvent->isValid) {
            $user->addErrors($beforeEvent->validationErrors);
            return false;
        }
        
        if ($user->save()) {
            // 生成激活令牌
            $activationToken = Yii::$app->security->generateRandomString();
            $user->updateAttributes(['activation_token' => $activationToken]);
            
            // 触发注册后事件
            $afterEvent = new UserAfterRegisterEvent([
                'user' => $user,
                'activationToken' => $activationToken,
            ]);
            $this->trigger(self::EVENT_AFTER_REGISTER, $afterEvent);
            
            return true;
        }
        
        return false;
    }
}
// 3. 事件监听器类
namespace app\listeners;

use Yii;
use app\events\UserAfterRegisterEvent;
use app\events\UserBeforeRegisterEvent;

class UserEventListener
{
    public static function handleBeforeRegister(UserBeforeRegisterEvent $event)
    {
        $user = $event->user;
        
        // 检查用户名是否已存在
        if (User::find()->where(['username' => $user->username])->exists()) {
            $event->validationErrors['username'] = ['Username already exists'];
            $event->isValid = false;
        }
        
        // 检查邮箱格式
        if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
            $event->validationErrors['email'] = ['Invalid email format'];
            $event->isValid = false;
        }
    }
    
    public static function handleAfterRegister(UserAfterRegisterEvent $event)
    {
        $user = $event->user;
        
        // 发送激活邮件
        self::sendActivationEmail($user, $event->activationToken);
        
        // 发送欢迎消息
        self::sendWelcomeMessage($user);
        
        // 记录注册统计
        self::logRegistration($user);
    }
    
    private static function sendActivationEmail($user, $token)
    {
        $activationLink = Yii::$app->urlManager->createAbsoluteUrl([
            'site/activate',
            'token' => $token
        ]);
        
        Yii::$app->mailer->compose('activation', ['link' => $activationLink])
            ->setTo($user->email)
            ->setSubject('Account Activation')
            ->send();
    }
    
    private static function sendWelcomeMessage($user)
    {
        // 发送系统消息
        $message = new Message();
        $message->user_id = $user->id;
        $message->title = 'Welcome!';
        $message->content = 'Thank you for registering!';
        $message->save();
    }
    
    private static function logRegistration($user)
    {
        Yii::info("New user registered: {$user->username} ({$user->email})", 'registration');
    }
}
// 4. 配置监听器 - 在应用配置中
return [
    'components' => [
        'userService' => [
            'class' => 'app\services\UserService',
            'on beforeRegister' => ['app\listeners\UserEventListener', 'handleBeforeRegister'],
            'on afterRegister' => ['app\listeners\UserEventListener', 'handleAfterRegister'],
        ],
        
        // 也可以使用行为来附加事件
        'mailer' => [
            'class' => 'yii\swiftmailer\Mailer',
            'as mailLogger' => [
                'class' => 'app\behaviors\MailLoggerBehavior',
                'on beforeSend' => function ($event) {
                    // 记录邮件发送
                    Yii::info('Sending email: ' . $event->subject);
                },
            ],
        ],
    ],
];
// 5. 控制器中使用
namespace app\controllers;

use Yii;
use app\services\UserService;

class UserController extends \yii\web\Controller
{
    public function actionRegister()
    {
        $userService = Yii::$app->userService;
        
        // 可以动态添加更多监听器
        $userService->on(UserService::EVENT_AFTER_REGISTER, function ($event) {
            // 发送短信通知(如果有手机号)
            if (!empty($event->user->phone)) {
                // 调用短信服务
            }
        });
        
        if ($userService->register(Yii::$app->request->post())) {
            Yii::$app->session->setFlash('success', 'Registration successful!');
            return $this->redirect(['site/index']);
        }
        
        return $this->render('register');
    }
}

八、最佳实践

  1. 命名规范

    • 事件名使用驼峰命名:EVENT_BEFORE_ACTION
    • 事件类名以 Event 结尾:UserSignupEvent
  2. 性能考虑

    • 避免在频繁触发的事件中执行耗时操作
    • 合理使用事件优先级
  3. 调试技巧

    // 查看已绑定的事件处理器
    Yii::debug(\yii\helpers\VarDumper::dumpAsString(
        Yii::$app->user->getEventHandlers('afterLogin')
    ));
    
  4. 测试事件

    // 单元测试中的事件测试
    public function testRegisterEvent()
    {
        $userService = Yii::$app->userService;
        $eventTriggered = false;
        
        $userService->on(UserService::EVENT_AFTER_REGISTER, 
            function ($event) use (&$eventTriggered) {
                $eventTriggered = true;
            });
        
        $userService->register($testData);
        
        $this->assertTrue($eventTriggered, 'Event should be triggered');
    }
    

总结

Yii2 的事件系统提供了强大的解耦机制,通过合理使用事件,可以使代码更加模块化、可维护和可扩展。建议将事件用于:

  • 记录日志和审计
  • 发送通知(邮件、短信等)
  • 数据验证和过滤
  • 业务逻辑扩展点
  • 状态变化监听
posted @ 2025-12-24 10:46  jintaonote  阅读(2)  评论(0)    收藏  举报