How Yii virtual attributes work

In this article I'd like to share some technical insights of a very useful aspect of Yii models: virtual attributes, how to use them correctly and avoid possible problems and headaches.

Although the actual content of the article is definitely targeted towards Yii developers, I'll try to link as much as possible to additional resources for anyone can actually explore the content a little bit more.

What's Yii?

Briefly, Yii is a lightweight PHP framework that implements ORM in a very similar way to RoR. On top of it, Yii does rarely impose any programming style/structure to its users and all the architectural decisions falls into the hands of the developers that are in total control of any aspect of their project.

If you feel intrigued by that and want to explore more, the documentation is great and to start with it I definitely recommend the definitive guide.

Virtual attributes

What I'm talking about shouldn't be anything new to Yii developers, if it is, there's a wiki article about understanding virtual attributes and get/set methods, which is totally worth reading.

This article could be considered a sort of technical addendum to it.

Yii Models (instances of CActiveRecord) are heavily relying on the database structure of the table they're representing (no attribute/field is defined in the class).

Virtual attributes, as you might imagine, lets you add additional attributes to the model even if they're not part of the actual database table. This structure provides great flexibility for many needs.

How do they work

Basic usage

Normally, when you need an additional attribute you would simply add a new property to the model and you're done, e.g.

Class Event extends CActiveRecord
{
    public $tempVal = null;
}

Trying to add an attribute on the fly with Yii, although possible in PHP world, will throw an Exception (more about that later).

With the above code you'll be able to read and write the property without problems on the model:

$model->tempVal = 'Everything is all right';
if ($model->tempVal !== null) {
    echo $model->tempVal;
}

Sometimes you need something more complex to be done on your attribute than just store a temporary value or perhaps return something based on the actual content of the model: virtual attribute lets you do so; you just need to modify the model by adding the getter/setter for it, as described in the following snippet of code:

Class Event extends CActiveRecord
{
    public function getPrettydate() {
        return date('r', $this->timestamp);
    }
}

and later access it with:

echo $event->prettydate;

Where headaches step in

If you try to add a getter/setter to the previously added public property you'll start realising the getter/setter won't actually work, it'll actually never be invoked.

Not only, if you're trying to do something on the value of a property before it is assigned using a setter, it wouldn't work either.

You'll see why in a few moments.

Solving the mystery

In order to achieve aforementioned nice things, Yii leverages on PHP __get() and __set() magic methods.

Any assignment or read of a property goes through them. Understanding how they work can help you quite a lot.

The stack of calls to make an assignment goes through these steps:

  1. $model->attribute = $value;
  2. CActiveRecord::__set($attribute, $value);
  3. CActiveRecord::setAttribute($attribute, $value);

At this point CActiveRecord::setAttribute() will:

  1. do assignment if the property exists in the model
  2. do assignment if the property exists as field in the db

if any of these is false, after returning, __set() will go on and

  1. see if there's any relations by that name in the model
  2. pass the call to parent::__set()

When stepping into the parent, i.e. CComponent::__set(), we have the real magic happening, where the call to the custom getter/setter is actually made.

If anything fails, e.g. setting a value where only the getter exists or where neither of them exists, will trigger an Exception.

By the way the whole things is structured, makes things in most of the cases nice and simple and I personally appreciate this in my daily work.

Something more

There are situations where you want to achieve something slightly more complex, something the system wasn't designed for, we could say.

A simple example would be to do come calculations on the value before it is assigned to the attribute, but this can be solved by implementing your own validator or either overriding beforeSave().

A more interesting example could be the necessity of using the value to be set to an actual field of the db, and incapsulate some specific logic to instead use another value stored somewhere else.

A solution I've used (and possibly not the only one I presume), would be to completely bypass by overriding __get() or __set().

Class Event extends CActiveRecord
{
    public $providedLocation = null;

    public function __set($name, $value) {
        if ($name === 'location') {
            $this->computeLocation($value);
        } else {
            parent::__set($name, $value);
        }
    }

    public function computeLocation($newValue) {
        // some complex logic to determine whether to assign
        // the $newValue or keep the old one
        if (...) {
            ...
            $this->location = $this->providedLocation;
        } else {
            $this->location = $newValue;
        }
    }
}

Performance and final remarks

Yii's proved to be really simple and intuitive, the abstraction built to provide the virtual attributes functionality has proven to be really well designed and with a small impact on performance.

Getters and setters are one of the last steps to be taken in the chain, and must be taken into the right consideration.

Of course you can't expect to be safe and sound with a huge amount of data coming out of your db with all kind of relationships and believe ORM will not impact the memory and overall performance of your app.

As usual the right design of the application should help you avoid bottlenecks, performance loss and tricks/hacks.
Knowing what you're using helps you leverage on it and take advantage as much as you think you need... Or at least that's what I'm doing with Yii.

Hope you enjoyed it till the end.