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

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

Under the Hood of Yii's Component Architecture

Welcome to the last article in this three-part whirlwind tour of the Yii framework’s component class. The goal of the series is to show you how Yii implements a component-based architecture and how its CComponent class handles the implementation details by making clever use of PHP’s magic methods.

In the first article you learned how Yii implements properties and a simple, but effective, configuration system. In the second, you learned how to implement event-based programming in your projects and how Yii uses magic methods to declare and bind events. In this article I want to focus on how you can use behaviors to dynamically modify the functionality of objects at runtime.

A behavior, as it is called in Yii, is a manner of combining objects at runtime to extend an object’s functionality. Behaviors are an excellent way to decouple code and keep ever expanding systems maintainable.

In the PHP object model, you can reuse functionality by extending a base class and the new class inherits the functions and properties from the parent. Inheritance is a great programming tool, however in complex systems it can very quickly become limiting. What happens when you have two or more classes with functionality relevant to a new class? We can not reuse our code effectively because PHP doesn’t support multiple inheritance. The Yii behavior system is a way of achieving multiple inheritance by implementing mixins.

Building Bits of a User

Let’s take a simple example. A new application you are building requires the user to connect to a third-party billing and credit card management service. You could dump this functionality into a generic user class, but before long your user class would grow very big and cluttered. Instead, it would be better to isolate the functionality and then only add it to a user object if its used. But how?

Aha! I know! Let’s add the functionality to a behavior! I bet you didn’t see that one coming.

First we create a UserAccountBehavior class. Behaviors need to extend from the CBehavior base class, and CBehavior extends from CComponent meaning behaviors themselves can also have behaviors, events, and properties — but let’s not get too carried away.

<?php
Class UserAccountBehavior extends CBehavior 
{
    public function getAccountInfomation() {
        // connect to a service to get credit card details and payment info
        return $paymentService->getDetails($this->owner->id);
    }
}

Inside the behavior, $this->owner returns the object that this behavior has been attached to. As you will only be attaching this behavior to user objects, you know $this->owner will be a user object (which in Yii is an instance of the CWebUser class). Therefore you can access the user’s ID by using $this->owner->id.

Now you need to attach the behavior to the user object at runtime. In Yii, the CWebUser class represents the logged in user. Yii creates a static instance of CWebUser and stores it in the user property of the application and you can refer to it using Yii::app()->user.

<?php
$user = Yii::app()->user;
$user->attachBehavior("account", new UserAccountBehavior());
echo $user->getAccountInformation();

There you have it, first you retrieved the user object using Yii::app()->user, then called the attachBehavior() method and passed in the behavior object to attach. The user object now has access to the additional methods defined by the behavior. In this example getAccountInformation() is called on the user object which in turn calls the getAccountInformation() method of the UserAccountBehavior object.

You can also access the UserAccountBehavior object itself using $user->account, so $user->account->getAccountInformation() would be the same as calling $user->getAccountInformation().

Lego Blocks Utilizing Behaviors and Events

A good example of where behaviors can be most useful is when combined with the active record pattern. Using behaviors, you can define a runtime records behavior by building it in a series of blocks.

For instance, an active record class representing a blog post may need to have several similar things. You may want it to have a “tagable” behavior so users can add tags and search for records using them. A tagable behavior might provide an easy to use findAllByTags(), getTags() and setTags() methods.

You may also want a soft delete behavior so when a user deletes a record a deleted flag is added but the record is not removed from the database itself to preserve historical data. The behavior can interact with its parent object (in this case the post record) and modify the standard delete functionality, but it may also expose a new deleteForever() method to permanently remove the record.

It’s worth noting here that behaviors can not override methods in its parent class as is normally the case with class inheritance, but you can write your code to allow events to handle such situations. For example, before running the class’ default delete method you can first check if the delete has already been handled. If it has, you know that some code somewhere (perhaps in a behavior) has dealt with the delete and has everything under control. If you cast your mind back to part two of this series you saw that I passed a CEvent object when raising events; this object has a handled property and here you would check to see if it is true or false. For example:

<?php
public function beforeDelete() {
    $event = new CEvent;
    // raises and calls events attached to "onBeforeDelete"
    $this->onBeforeDelete($event);
    if ($event->handled) {
        return true;
    }
    else {
        return $this->_delete();
    }
}

Now for the soft delete behavior you can attach the onBeforeDelete() method, set the deleted property in the database and set the event object’s handled property to true.

Yii provides a CActiveRecordBehavior behavior which exposes and automatically binds methods to the following events: onBeforeSave, onAfterSave, onBeforeDelete, onAfterDelete, onBeforeFind, and onAfterFind. When creating behaviors for active record objects, you can extend from this class. Note that you can also manipulate the beforeFind() and afterFind() methods to modify the retrieval query criteria to prevent records marked as deleted from being returned.

Another example might be that you want this record to represent nodes within a tree so you can create a hierarchy of posts; you can add a tree behavior giving access to additional methods such as addChild(), getChildren(), getAncestors(), etc. The list of behaviors could go on and on forever, but the best part is that these can be stored as individual files like little Lego blocks enabling you to build up objects with advanced functionality from existing code very quickly.

Yii’s Magic

Now I’ll dive straight into the implementation details of how Yii creates its behavior system. This will give you an understanding of what is happening under the hood so that you can better understand how to use behaviors in your own programming. You’ll also see how you might implement your own behavior system in your own framework or project.

The behavior system will need the following ingredients:

  • A method of attaching an object as a behavior
  • A method of storing all attached behaviors on an object
  • Some magic methods to work out if the functionality has been delegated to one of the attached behavior objects
  • A method to remove behaviors

Let’s start with a method for attaching behaviors:

<?php
public function attachBehavior($name, $behavior) {
    $behavior->attach($this);
    return $this->_m[$name]=$behavior;
}

This function to attach a behavior is quite simple; it calls the behavior’s attach() method which is defined by the CBehavior base class in Yii. It receives a pointer to the current object $this to allow the behavior to track which object it is attached to.

Inside the CBehavior class which all behaviors must extends is the definition of the attach() method:

<?php 
public function attach($owner) {
    $this->owner = $owner;
}

As I mentioned earlier, this allows you to use $this->owner inside your behavior object to refer to it’s owner (the object it is currently attached to).

Next, a method of storing all attached behaviors is needed. This has actually already been demonstrated in the attachBehavior() method with this code $this->_m[$name] = $behavior. To store a list of behaviors, you simply save them to an associative array keyed by the behavior name. Now you can attach behaviors to any object that subclasses CComponent.

Now there needs to be a way to enable interacting with these behaviors and call the correct methods when necessary. The PHP magic __call method suits this perfectly.

<?php
public function __call($name,$parameters) {
    if ($this->_m!==null) {
        foreach($this->_m as $object) {
            if (method_exists($object,$name)) {
                return call_user_func_array(array($object, $name), $parameters);
        }
    }
    throw new Exception(get_class($this) . " and its behaviors do not have a method named '" . $name . "'");
}

When calling an undefined method of an object, PHP will invoke the magic __call method. The name of the method that was called is passed in as the $name argument and the arguments that were in the method call are passed in as $parameters. Now all the code needs to do is simply loop through all the attached behaviors (stored in $this->_m) and check if they contain a method with the same name as was just called. If the method does exist in a behavior, then it is invoked and the results are returned. If no matching method is found in any of the behaviors, an exception is thrown complaining about just that.

In a nut shell, that’s it – your very own behavior system. I’ve based these examples heavily on code in the CComponent class of the Yii framework, and simplified them a fair bit for the purpose of this article, but feel free to download the framework and have a peruse yourself.

Summary

Together we have gone on a journey to discover ways we can better encapsulate our code and make reusable components. Yii’s component class shows how easy you can achieve very complex and clever patterns in PHP with relative ease, from simple properties and configuration to events and behaviors.

I hope you’ve enjoyed this three-part series and you now have a few more tools at your disposal in your programming toolbox. If you have never used events or behaviors in your programming before, it’s definitely worth taking these ideas out for a test drive. As with most programming the concepts do not fully sink in until you try them out yourself and apply them in real world situations.

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

  • Imre

    Thanks, brilliant explanation of how behaviours work.

  • Senguttuvan G

    Awesome explanation of behaviours with respect to right context.

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

    Somehow missed this when it came out, but glad I’ve found it now.

    Have to take my hat off to this Steve guy – another great article – fantastically clear and simple.

    Correct me if I’m wrong, but I guess the main point about behaviours is that it’s an easy way to implement aggregation which in other languages (e.g. C++) used to be quite difficult. The Yii implementation in the magic __call method takes away the horrendous pain and makes it unbelievably easy.

    One thing maybe worth noting is, from the Yii implementation – the first behaviour that implements the method will be used so need to be careful that the behaviours don’t duplicate method names (I guess).

  • Cherif BOUCHELAGHEM

    Great series, I enjoy it even Im regular developer with Yii, just I want to share something,when developing a behavior for a model it’s better to extend it from CActiveRecordBehavior because it has more defined events (beforeDelete, afterSave, etc..) .

    from the Api: http://www.yiiframework.com/doc/api/1.1/CActiveRecordBehavior

    CActiveRecordBehavior is the base class for behaviors that can be attached to CActiveRecord. Compared with CModelBehavior, CActiveRecordBehavior attaches to more events that are only defined by CActiveRecord.

  • http://trickortip.com Andres

    Steve,

    Thank you for taking the time to write this post. I just finished the three article series and learned a lot about the basics of Yii components.

    I have a question for you. You say we need to attach the behaviors to the components at runtime. In your experience when is the best moment to do this if you are using them as legos. For example if you already know that blog posts should be taggable all the time, when would you do this?

    Thanks,

    Andres

    • http://newicon.net Steve O’Brien

      Hi Andres,

      Gosh… The place really depends on the structure of your code. It could be on startup or initialisation of the class or mid flow in the logic depending on the use case. However if you had a post model extending from CActiveRecord class then you could use the behaviors function too. http://www.yiiframework.com/doc/api/1.1/CModel#behaviors-detail You specify an array of behaviors you would like your model to have, then when Yii instantiates model instances it will automatically attach the listed behaviors, and you never need to worry about it.

  • Ejaz karim

    Nice tutorial. Can I access behavior’s private property from component to which it is attached?