如何在Yii框架中实现拉取式事件而非推送式事件?
Got it, let's figure out how to implement a pull-based event system in Yii instead of the default push-based setup you're currently using. The key issue with your current code is that the event trigger (MyUser) is directly binding handlers, which feels more like nested method calls than a properly decoupled event pattern. You want the trigger to just fire the event, and let target models like UserRole independently detect and react to it without pre-registering handlers in MyUser's init() method. Here are two practical approaches to make this happen:
方案1:利用Yii类级事件实现解耦(接近拉取式的关注点分离)
Yii supports class-level event binding, which lets handler models (like UserRole) subscribe to events on their own—no need for the trigger model (MyUser) to know anything about them. This keeps MyUser focused only on firing events, while other models decide if and how to respond.
Step 1: Clean up the MyUser model
Remove the handler binding from init()—we just need the event definition and trigger logic:
<?php namespace app\models; use Yii; class MyUser extends \yii\db\ActiveRecord { const EVENT_NEW_USER = 'new-user'; /** * @inheritdoc */ public static function tableName() { return 'user'; } /** * @inheritdoc */ public function rules() { return [ [['name', 'email'], 'string', 'max' => 255] ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => 'ID', 'name' => 'Name', 'email' => 'Email', ]; } // No more handler binding here—we're just the event source }
Step 2: Let UserRole subscribe to the event on its own
In your UserRole model, add code to subscribe to MyUser's event during its own initialization:
<?php namespace app\models; use Yii; class UserRole extends \yii\db\ActiveRecord { /** * @inheritdoc */ public static function tableName() { return 'user_role'; } public function init() { parent::init(); // UserRole decides to listen for new user events // Class-level binding means all MyUser instances will trigger this handler MyUser::on(MyUser::EVENT_NEW_USER, [$this, 'assignDefaultRole']); } public function assignDefaultRole($event) { // Get the user that triggered the event $newUser = $event->sender; // Create a default role entry for the new user $this->user_id = $newUser->id; $this->role = 'default'; // Adjust to your default role $this->save(false); // Skip validation if needed, or add proper rules } // Add your other UserRole methods here }
Step 3: Keep your event trigger logic intact
Your existing action code stays the same—just fire the event, no need to worry about who's listening:
public function actionTestEvent() { $model = new MyUser(); $model->name = "John"; $model->email = "john@gmail.com"; if($model->save()) { // Just trigger the event—handlers will take care of the rest $model->trigger(MyUser::EVENT_NEW_USER); } }
Bonus: Add admin email notifications without touching MyUser
If you still need to send admin emails, create a separate class to handle that—no changes to MyUser required:
<?php namespace app\models; use Yii; class AdminNotificationService { public function __construct() { // This service subscribes to new user events on its own MyUser::on(MyUser::EVENT_NEW_USER, [$this, 'sendNewUserAlert']); } public function sendNewUserAlert($event) { $user = $event->sender; echo "Alert sent to admin: New user registered - {$user->name} ({$user->email})"; // Replace with actual email sending logic using Yii's mailer } }
方案2:纯拉取式事件(基于事件日志存储)
If you want a true pull-based system where events are just recorded, and handler models actively check for and process them (no pre-subscription needed), use an event log table to persist events.
Step 1: Create an event log model and table
First, set up a table to store events:
CREATE TABLE `event_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `event_name` varchar(255) NOT NULL, `event_data` text NOT NULL, `created_at` int(11) NOT NULL, `processed` tinyint(1) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Then create the corresponding EventLog model:
<?php namespace app\models; use Yii; use yii\db\ActiveRecord; class EventLog extends ActiveRecord { public static function tableName() { return 'event_log'; } public function rules() { return [ [['event_name', 'event_data', 'created_at'], 'required'], [['event_data'], 'string'], [['created_at', 'processed'], 'integer'], [['event_name'], 'string', 'max' => 255], ]; } public function beforeSave($insert) { if (parent::beforeSave($insert)) { if ($insert) { $this->created_at = time(); $this->processed = 0; } return true; } return false; } // Helper method to log events public static function logEvent($eventName, $data) { $logEntry = new self(); $logEntry->event_name = $eventName; $logEntry->event_data = json_encode($data); $logEntry->save(); } }
Step 2: Log events in MyUser instead of triggering them
Modify MyUser to write to the event log when a new user is created:
// Add this method to MyUser public function afterSave($insert, $changedAttributes) { parent::afterSave($insert, $changedAttributes); if ($insert) { // Log the new user event instead of triggering push-based events EventLog::logEvent(self::EVENT_NEW_USER, [ 'user_id' => $this->id, 'name' => $this->name, 'email' => $this->email ]); } }
Step 3: Let UserRole pull and process events
Add a method to UserRole that checks for unprocessed events and handles them. You can call this method via a cron job, console command, or even in your web app's bootstrap:
// Add this static method to UserRole public static function processNewUserEvents() { // Fetch all unprocessed new user events $events = EventLog::find() ->where(['event_name' => MyUser::EVENT_NEW_USER, 'processed' => 0]) ->all(); foreach ($events as $event) { $eventData = json_decode($event->event_data, true); // Avoid duplicate roles if (!self::find()->where(['user_id' => $eventData['user_id']])->exists()) { $role = new self(); $role->user_id = $eventData['user_id']; $role->role = 'default'; $role->save(false); } // Mark event as processed $event->processed = 1; $event->save(false); } }
Which approach should you use?
- 方案1 is great for most cases: it uses Yii's native event system, keeps code clean, and fully decouples event sources from handlers.
- 方案2 is ideal for asynchronous or batch processing, or if you need to guarantee events are never lost (even if the system restarts).
内容的提问来源于stack exchange,提问作者Lovlesh Yadav

