Search code examples
yiiyii2

Hook into User Save from a separate module


I'd like to use modules to separate code in a large monolithic Yii app. Let's say there is a core module which has a User model, and I'm creating a new audit module which tracks user updates.

The audit module has an AuditLog model which stores information about changes to the User model.

If I wasn't trying to keep the audit context separate, I could just add the audit logic to User->afterSave().

What is the most idiomatic way in Yii to react to a user being saved, without adding Audit specific logic to the User model?


Solution

  • As suggested by Insane Skull in comments, you should use events to handle audit tasks when user is changed.

    There already exist yii\db\BaseActiveRecord::EVENT_AFTER_INSERT and yii\db\BaseActiveRecord::EVENT_AFTER_UPDATE events that are triggered when any object that inherits from yii\db\ActiveRecord is saved.

    You can create handler for this events in your audit module, for example like this:

    namespace app\modules\audit\components;
    
    class UserChangeHandler extends \yii\base\Component
    {
        public function handleInsert(\yii\db\AfterSaveEvent $event)
        {
            // ... your audit logic when new user is created ...
            // original User model is available in $event->sender
        }
    
        public function handleUpdate(\yii\db\AfterSaveEvent $event)
        {
             // ... your audit logic when user is updated ...
        }
    }
    

    Then you can bind the events in audit module bootstrap like this:

    namespace app\modules\audit;
    
    use app\modules\audit\components\UserChangeHandler;
    use app\models\User;
    use yii\base\Event;
    use yii\db\BaseActiveRecord;
    
    class Module extends \yii\base\Module implements \yii\base\BootstrapInterface
    {
        private UserChangeHandler $handler;
    
        public function __construct($id, $parent, UserChangeHandler $handler, $config = [])
        {
            parent::__construct($id, $parent, $config);
            $this->handler = $handler;
        }
    
        public function bootstrap($app)
        {
            Event::on(
                User::class,
                BaseActiveRecord::EVENT_AFTER_INSERT,
                [$this->handler, 'handleInsert']
            );
            Event::on(
               User::class,
               BaseActiveRecord::EVENT_AFTER_UPDATE,
               [$this->handler, 'handleUpdate']
            );
        }
    
        // ... other definitions in module class ...
    }
    

    Then in your application config you add your audit module and tell the app that it should bootstrap it like this:

    return [
        'bootstrap' => [
            //... other bootstraped components/modules ...
            'audit',
        ],
        'modules' => [
            // ... other modules ...
            'audit' => \app\modules\audit\Module::class,
        ],
        // ... other configurations ...
    ];
    

    This way every time the after insert, or after update events are invoked in user model, your handler will be executed. Your core application is independent on audit plugin, it's only referenced in application configuration.

    NOTE: The EVENT_AFTER_UPDATE and EVENT_AFTER_INSERT are only triggered when save(), update() or insert() methods are used. If the user record is edited by using static updateAll() method or in another way that doesn't trigger afterSave() callback those events won't be triggered. If you are updating users using these methods too you might want to add your own event and trigger it when the change happen.