Under the Hood of Yii’s Component Architecture, Part 2

This entry is part 1 of 3 in the series Under the Hood of Yii's Component Architecture

Under the Hood of Yii's Component Architecture

Welcome once again to this tour of the Yii Framework’s CComponent class. This three-part series demonstrates how Yii uses a component-based architecture and how the class implements properties, configuration, events, and behaviors. In Part 1 you saw how Yii uses PHP’s magic methods to implement component properties. In this article, which is Part 2 of the series, I’ll show you how you can do event-based programming in PHP. I’ll also show you how the component class makes it possible.

Events

An application event is something that occurs which might be of interest to other bits of code. A standard event in most GUI applications would be a “click” event, but the sky’s the limit and what events you define is really up to you. For example, when a user registers you’ll often want to send her an email welcoming her to your application. You could define “Registration complete” as an event. Rather than hard-coding email logic or even setting different user properties in your code, the module could simply raise an event and forget about any specific implementation details. The details can be provided by application-specific modules allowing you to keep individual requirements separate from your reusable code. Events allow you to attach a potentially unlimited amount of functionality without changing your core modules and components.

There are generally three steps to implementing an event:

  1. Define the event
  2. Attach custom functionality to the event
  3. Trigger the event when it happens

Defining Events
First you need to define the event allowing Yii to attach functions to it. Continuing with the user registration example, you would create a new event arbitrarily called onUserRegistered that is raised after a user has been successfully registered. This would be defined within my your user module’s code.

<?php
public function onUserRegistered($event) {
    $this->raiseEvent("onUserRegistered", $event);
}

Events in Yii are defined by simply adding a function with “on” prefixed to the method name. In order to attach a function to an object you need to be able to access the object from other areas of the application. The event is added to the user component so that it can be accessed throughout the application through Yii::app()->user.

Triggering Events

The controller class of the user module would be responsible for triggering the event when it actually occurs.

<?php
public function actionUserRegister() {
    // code to register a user and add to the database
    // ...
    // user has been successfully registered so lets raise an event to tell the world
    $e = new CEvent($this, array("user" => $user));
    Yii::app()->user->onUserRegistered($e);
}

Once a user has been successfully registered and added to your database, you’ll want to tell the world. This code raises the event. Events do not know the specific details of the function they are calling so all handling functions must have the same interface. In Yii, all handling functions expect one parameter which is a CEvent object. The CEvent object expects two parameters: a reference to the object that raised the event, and an optional array of parameters that can be used to store specific information relating to the particular event (these parameters can be accessed later by your handling functions). In this example I want all handling functions to be able to access the user object that was just registered. To do this I pass an array of array("user" => $user) where $user is an object representing our recently registered user. I then trigger the event and evoke Yii’s raiseEvent() function by calling the function onUserRegistered() and pass in the event object.

That is all the code that needs to be added to the user module. Now any additional code anywhere in your application can attach additional functionality to be executed when a new user registers.

Attaching Events

Continuing with the example, let’s see how attach an event’s callback.

<?php
public function init() {
    Yii::app()->user->onUserRegistered = array($this, "sendMyEmail");

}

public function sendMyEmail($event) {
    $user = $event->params["user"];
    mail($user->email, "Welcome to this amazing event app", "Hello...");
}

I’ve attaching the sendMyEmail() method to the onUserRegistered event so now when a new user registers the sendMyEmail() function will be called. Alternatively, in PHP 5.3 or greater you can specify an anonymous function. Yii supports assigning any value to an event that would also return true if passed into the PHP is_callable() function.

Yii’s Magic

Now let’s look at how Yii implements events; managing events is all handled by cleverness in the CComponent class. In order to implement events the component class has to implement the three main concepts:

  • Defining events – Yii need to store or be able to lookup defined events.
  • Triggering events – When an event occurs, Yii need to call all of our PHP functions attached to the event.
  • Attaching events – Yii need a mechanism to store a list of valid PHP callbacks against an event.

The mechanism for defining events in Yii is to simply create a function with the “on” prefix as you saw earlier. The mechanism for triggering an event is to add $this->raiseEvent("onMethodName"); within the defined event and simply call the method when the event occurs in your code. This leaves us with two implementation details left:

  • Attaching functions to the event.
  • Calling all functions attached to the event when it is triggered.

To attach the event you use the code onMyEventName = callback, which means the implementation for attaching functions to events must be handled in the components magic __set method.

<?php
public function __set($name, $value){
    if (strncasecmp($name, "on", 2) === 0 && method_exists($this, $name)) {
        $name = strtolower($name);
        if (!isset($this->_e[$name])) {
            $this->_e[$name] = new CList();
        }
        return $this->_e[$name]->add($value);
    }
}

The implementation first checks if the value of $name starts with the text “on” and that a method also exists with the same name as the value. If it does, Yii assumes $value is a representation of a callback which it needs to attach to the event defined by $name. Yii has a private member variable $_e which holds an array of callbacks keyed by event names, and it simply adds the callback to the list for the particular event key.

$_e => array(
    'onUserRegistered' => array(
        0 => array(object, 'sendMyEmail')
    ),
    'onSomeOtherEvent'=>array(
        0 => function(){}
        1 => ...
    )
)

All that’s left now is to call the attached functions when the event is triggered. From looking at the array of stored events, this could be as easy as looking up the event in the $_e variable, and iterating through each callback in the array and running it.

public function raiseEvent($name, $event) {
    foreach ($this->_e[$name] as $handler) {
        if (is_array($handler)) {
            // an array: 0 - object, 1 - method name
            list($object, $method) = $handler;
            $object->$method($event);
        }
        else {
            // PHP 5.3+ anonymous function
            call_user_func($handler, $event);
        }
    }
}

I’ve refactored the raiseEvent() method so it’s easier to read for our demo, but the Yii implementation has more error checking and handles additional more callback types, such as static methods.

Summary

Events are a great concept to implement for robust, flexible code. In this article you’ve learned how to create reusable components that can have their functionality extended through the use of events, and have seen seen how the Yii framework’s CComponent class implements and manages events in PHP. Of course, the usage and implementation details may be specific to Yii but the concept itself is universal.

This wraps up our second article in this three part series. The final article walks through creating and implementing behaviors, another great way to extend a component’s functionality while keeping it easy to reuse.

Image via Filipchuk Oleg Vasiliovich / Shutterstock

Under the Hood of Yii's Component Architecture

Under the Hood of Yii’s Component Architecture, Part 1 >>

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • pawan

    Awesome again.
    Waiting for part3.

  • http://www.whosup4.com Neill Jones

    Thanks Steve! Great article again.
    Initially found the various stages confusing (i.e. why you set onUserRegistered twice – once as a method and once as a variable) until I got to the end of the article when it all made sense through the use of __set and the useful ability to assign multiple handlers to the same event. Also thought the way you described its usefulness in creating flexible reusable code was an important point to emphasise!

  • Antony

    Based on this implementation, the whole process appears to be synchronous in nature, meaning that the more event handler you attach, the longer it takes to finish a task?
    Still need to find a good balance as to when to use event vs simply adding code in the same code block.

    • http://steve-obrien.com steve

      The main reason I use events is for code reusability. When building reusable blocks or modules of code events become a great way to enable past and potential future applications from modifying behavior without changing the modules core code.

  • bear

    A question: Where to put the code of Attaching Events?
    As the sample:
    user->onUserRegistered = array($this, “sendMyEmail”);

    }

    public function sendMyEmail($event) {
    $user = $event->params["user"];
    mail($user->email, “Welcome to this amazing event app”, “Hello…”);
    }

    • http://steve-obrien.com steve

      Hi bear,
      It’s up to you really, you need to put the code that attaches the event somewhere probably on initialisation before the whole app is fired up and events start firing. Also you need to be able to access the objects in order to attach to them. Of course you don’t have to have the event handler code on the object itself. For example a user object that is only created later in the application could raise events on a parent module class. This way other objects can attach to these.

      Personally when building an app I break each functional area into modules. Yii has a nice modular architecture out of the tin. So I have a User module that contains a UserModule.php class I tend to add my event definitions to this class. So objects within this module raise events on the UserModule class as this class is available to the rest of the application…. hope that helps.

  • jevens
    • http://steve-obrien.com steve

      Hi Jevens,
      Yes that’s a nice implementation.
      You can effectively use Yii in the same way using annonymous functions in PHP 5.3+ and you can use the underlying $this->attachEventHandler (similar to Proems $this->attach method) and $this->raiseEvent (similar to Proems $this->trigger)

      One thing I didn’t explain in the article very well is the advantage of defining events that you can attach to as functions. Though I love events and they solve a few maintenance and application scaling issues they can cause a different maintenance problem. When you start using events everywhere, it gets very difficult to track whats calling what where and even to find a list of events that you can attach to. Defining the events on the class as methods with the on prefix like ‘onButtonPress’ really helps in both auto documentation and from a sanity point of view. Couple this with a convention of where you put your event attach code and events become your best friend in big projects.

  • sukhwinderSingh

    hi
    thanks for nice tutorial, i was too confused in understanding the events. I have one query, in the __set() function we have following lines :
    if (!isset($this->_e[$name])) {
    $this->_e[$name] = new CList();
    }
    return $this->_e[$name]->add($value);

    So the property _e[$name] is storing the instance of class CList. But bellow this you have explained that _e is array like :
    $_e => array(
    ‘onUserRegistered’ => array(
    0 => array(object, ‘sendMyEmail’)
    ),
    ‘onSomeOtherEvent’=>array(
    0 => function(){}
    1 => …
    )
    )
    I am just confused to see that how _e is an array? it should be an object of class CList.
    Thanks in advance

  • http://www.irseoart.com Seoart Iran

    Hi steve,
    I Want to know Who Can i Show User Messages When They Login Whith Events

    i Added Component Like Bellow
    class emailSender extends CComponent
    {
    public function onLog($event)
    {
    $this->raiseEvent(‘onLog’,$event);
    }
    //new event handler wich i call in controller
    public function show($event)
    {
    echo ‘Hi,’.Yii::app()->name.’, wellcome';
    }
    //new user handler
    public function userwellcome($event)
    {
    echo ‘user information’.Yii::app()->user->guestName;
    }
    }

    so i add init() in Controller
    public function init()
    {
    $this->email=new emailSender();
    $this->email->onLog=array($this->email,’show’);
    $this->email->onLog=array($this->email,’userwellcome’);
    parent::init();
    }
    till here i just need call $email->onLog(new CEvent($this))
    but i dont know where can i call this to show user message
    i did it in actionLogin() like below
    if($model->validate() && $model->login())
    {

    $this->redirect(Yii::app()->user->returnUrl);
    $this->email->onLog(new CEvent($this));
    }

    but when it redirects i cant see anything, i whish i could explain problem
    please help me how can i call best approach and not just hard coding

    طراحی قالب وردپرس ؛ طراحی سایت

    • http://newicon.net Steve

      Hi there Seoart,
      I would extend CWebUser, then extend the afterLogin function. You could then raise your event in there. Alternatively fire the event in the controller that handles the login process.

      I would recommend changing where the event is defined though, I would call it onUserLogin and define it in a class that is responsible for managing user related tasks. At the moment you define it in your email class which could be confusing in the future.

  • http://codeduniya.in/ Rajesh

    I am newbie in yii framework. This post really helped me. Thanks a lot for sharing this.