Watching for Model Events in the Yii Framework

March 13, 2013
The Yii Book If you like my writing on the Yii framework, you'll love "The Yii Book"!

Much of the work done with models involves using the methods defined within the model classes. These methods, such as rules() and relations(), are created by the code generator Gii. You’ll also add your own methods to the code generated for you. But, thanks to inheritance, there are lots of methods common to Yii models that you’ll frequently use. In this post, I want to specifically look at:

  • afterConstruct()
  • afterDelete()
  • afterFind()
  • afterSave()
  • afterValidate()
  • beforeDelete()
  • beforeFind()
  • beforeSave()
  • beforeValidate()

These methods are used to handle model-related events. Before looking at the usage of these methods, let’s first look at event handling in Yii in general.

This is an excerpt from Chapter 5, “Working with Models,” of “The Yii Book“.

The CComponent Class

Something I thought about discussing in Chapter 3, “A Manual for Your Yii Site,” but later changed my mind about, is the concept of components. Discussion of components can get a bit complex (which is why I removed it from Chapter 3), but components are an important subject, and it’s time they were introduced to you.

Unlike the application components configured in Chapter 4 (such as the database component, the “urlManager” component, and so forth), I’m talking about generic components here. Components are the key building block in the Yii framework. It all starts with Yii’s CComponent class. Most of the classes used in Yii are descendants of the base CComponent class. For example, the application object will be of type CWebApplication. That class is derived from CComponent (although there are other classes in between). Controllers are of type CController, which inherits from CBaseController, which inherits from CComponent. CActiveRecord inherits from CModel, which inherits from CComponent, and the same inheritance path apply to CFormModel.

Part of Yii's class inheritance structure, with CComponent at the top.

Part of Yii’s class inheritance structure, with CComponent at the top.

Knowing that the component is the basic building block is important to your use of Yii. Because of the nature of inheritance in OOP, functionality defined in CComponent will be present in every derived class, which is to say most of the classes in the framework.

The CComponent class provides three main tools:

  • The ability to get and set attributes
  • Event handling
  • Behaviors

Of these three, I want to discuss events now. This coverage will be specific to models, but understand that any class that inherits from CComponent supports events (which is to say most classes).

Event Handling in Yii

Event programming isn’t necessarily familiar territory to PHP developers, as PHP does not have true events the way, say, JavaScript does. In PHP, the only real event is handling the request of a PHP script (through a direct link or a form submission). The result of that event occurrence is that the PHP code in that script is executed. Conversely, in JavaScript, which continues to run so long as the browser window is open, you can have your code watch for, and respond to, all sorts of events (e.g., a form’s submission, the movement of the cursor, and so forth). Thanks to the CComponent class, Yii adds additional event functionality to PHP-based Web site.

Event handling in any language starts by declaring “when this event happens with this thing, call this function”. In Yii, you can create your own events, but models have their own predefined events you can watch for: before a model is saved, after a model is saved, before a model is validated, after a model is validated, and so forth (the available events will depend upon the model type). Yes: each of the methods previously mentioned correspond to an event that Yii will watch for with your models.

In many situations, you’ll want to make use of events when something that happens with an instance of model A should also cause a reaction in model B. You’ll see examples of this in time. But watching for events can be a good way to take some extra steps within a single model, too.

For example, you might want to do something special before a model instance is saved. To do so, just create a beforeSave() method within the model:

# protected/models/SomeModel.php
protected function beforeSave() {
    // Do whatever.
    return parent::beforeSave();
}

As you can see in that minimal example, it’s a best practice to call the parent class’s same event handler (here, beforeSave()) just before the end of the method. Doing so allows the parent class’s event handler to also take any actions it needs to, just in case. (If you don’t do this, then any default behavior in the parent class method won’t be executed.)

As a real-world example of using an event with a model, the page.user_id value needs to be set to the current user’s ID when a new page record is created. One way to do that is to create a beforeValidate() event handler that sets the attribute’s value:

# protected/models/Page.php
protected function beforeValidate() {
    if(empty($this->user_id)) { // Set to current user:
        $this->user_id = Yii::app()->user->id;
    }
    return parent::beforeValidate();
}

This does assume that the current user’s ID is available through user->id, but other than that, it will work fine. And because this method checks for an empty user_id attribute first, the event handler will not have an impact on the attribute’s value when a page is being updated.

The same concept can be applied to the user_id and page_id attributes in Comment and the user_id attribute in File. See the downloadable code for examples of all of these.

As another example, earlier in the chapter you saw how to set the two date/time column values via scenarios:

# protected/models/AnyModel.php::rules()
array('date_entered', 'default', 'value'=>new CDbExpression('NOW()'), 'on'=>'insert'),
array('date_updated', 'default', 'value'=>new CDbExpression('NOW()'), 'on'=>'update'),

An alternative solution would be to use the beforeSave() method and set the values within it. To test whether this is an insertion of a new record or an update of an existing one, the code can check the isNewRecord property of the model:

# protected/models/AnyModel.php
public function beforeSave() {
    if ($this->isNewRecord) {
        $this->created = new CDbExpression('NOW()');
    } else {
        $this->modified = new CDbExpression('NOW()');
    }
    return parent::beforeSave();
}

Which approach you take for setting values–validation scenarios or events–is largely a matter of preference, as both will can the trick. The argument for using event handling is that you are moving more of the logic out of the rules and into new methods, which can make for cleaner code.

As a final note on this concept, if the event that’s about to take place shouldn’t occur–for example, the model should not be saved for some reason, just return false in the event handler method.