Understanding Virtual Attributes and get/set methods

  1. An Easy Virtual Attribute
  2. Getters and Setters in Detail
  3. Resolving Conflicts
  4. Do not use get and set yourself!
  5. PHP Dynamic Attributes Don't Work

When you define or extend a class, you can create class variables and methods in Yii just like you can in any other PHP system:

class Comment extends CActiveRecord {
    public $helperVariable;
    public function rules() { ... }
    ...
}

and then use them in the obvious way:

$var   = $model->helperVariable;
$rules = $model->rules();

This part everybody understands.

But Yii provides access to lots of other things via an instance variable, such as database fields, relations, event handlers, and the like. These "attributes" are a very powerful part of the Yii framework, and though it's possible to use them without understanding, one can't really use the full power without going under the covers a bit.

An Easy Virtual Attribute

Before digging into the mechanisms of how it all works, we'll look at an example to illustrate the point.

Scenario: your application has a model for a Person -- an actual human being -- and the database has separate fields for first and last name: ~~~ [sql] CREATE TABLE person ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, firstname VARCHAR(32), lastname VARCHAR(32), ... ) ~~~ Yii's Active Record maps these easily into the Person model, which allows you to reference and assign $model->firstname and $model->lastname attributes anywhere in your code. ActiveRecord is one of the coolest feature of Yii.

But it's common to need to refer to the firstname + lastname pair as a single unit (in Views, certainly) so you find yourself doing:

$name = $model->firstname . " " . $model->lastname;

all over to get the full name. Though this is straightforward, it's nevertheless tedious, and it would be nice to optimize it. Let's do just that.

Yii treats functions beginning with "get" as special, so let's make one to provide the full name in a single step:

class Person extends CActiveRecord {
   public function getFullName()
   {
      return $this->firstname . " " . $this->lastname;
   }
   ...
}

With this getter function defined, $model->fullname automatically calls the function and returns the value as if it were a real attribute: this is a virtual attribute, and it's very powerful.

Though a getter function cannot be assigned to, its value can always be fetched from anywhere in the code, including in CHtml::listData when creating a dropdown list. It's quite common to want to display multiple parts of a model record to the user even though it nevertheless selects just a single ID:

// in a view somewhere
echo $form->dropDownList($model, 'personid',
    CHtml::listData( Person::model()->findAll(), 'id', 'fullname' )
);

Now, the dropdown will show full user names in the dropdown, which makes for a better user experience. Attempting to provide a dropdown including firstname + lastname without a model helper function like this is more work and less useful.

EXTRA BONUS - In the future, if you add a MiddleName to the Person database table, you only have to modify the getFullname() method in order to automatically update all the views that use $model->fullname.

This is in addition to the benefit of $model->fullname being more clear in the first place.

Getters and Setters in Detail

The previous section showed this by example -- which we hope piques your interest - but it's important to know how it works.

When a program references $model->anything, PHP checks to see if there is a class member variable by the name anything. If it's there, it is used directly and that is the end of the matter.

But if the name is not known PHP (since version 5) invokes the magic method __get on the class, giving it a chance to handle it in application code. This method can decide to handle the attribute (returning a value), or decline to handle it, which produces a PHP error.

Yii's CComponent class, the base of most other classes, contains the support for the magic __get method, and it uses this to provide the rich features we all know and love: relations, database fields, virtual attributes, and so on.

Since none of these things is a direct class variable (which PHP handles directly), the __get method is called -- Yii implements this in the base class as CComponent::__get() -- with the name of the unknown variable as a parameter.

Yii runs through its internal state and database metadata, looking for get/set virtual attributes, database fields and relations, and the like. If one is found, the search stops and the value is returned to the user. If the name is not known, then it fails the request with an unknown attribute (the same as if the __get method was not defined).

The analog of the getter is the setter, a function that takes an attribute name and a value, and Yii calls the function automatically:

public function setSomething($value) { ... }

This allows $model->something = $value to work seamlessly.

Note: It is not required to define matching get and set virtual attributes in a class: just define what you need (indeed, though getFullname() makes sense, setFullName() does not).

Note that the get/set functions can be called directly as functions (but requiring the usual function-call parentheses):

$x = $model->fullname;
$x = $model->getFullname();        // same thing

$model->active = 1;
$model->setActive(1);              // same thing

Note that Yii uses get/set functions very heavily internally, so when reviewing the class references, any function beginning with "set" or "get" can generally be used as an attribute.

Resolving Conflicts

When using an attribute name -- $model->foo -- it's important to know the order in which they are processed, because duplicates are not generally detected, and this can cause all kinds of hard-to-find bugs.

When there are conflicts or duplicates, there has to be some order in which the attributes are resolved. This is hard to pin down generally, because Yii versions change over time, and each class that overrides __get and __set imposes its own additional interpretations.

But in the most common case of CActiveRecord, this is the oversimplified resolution order:

  1. Direct class member variables are always interpreted by PHP before anything else, and the __get method is not called. This is very fast access, but not flexible. If there is a get/set function, a relation, a database field, etc. with the same name, it's completely ignored. No class can override direct class member variable access.
  2. Database Fields (in CActiveRecord)
  3. Database Relations (in CActiveRecord)
  4. Virtual Attributes defined with get/set functions (in CComponent)
  5. Events called with functions starting with on (onBeforeSave, etc.)

Those wishing to refine this are encouraged to visit the source code of [CComponent] and [CActiveRecord]

Do not use get and set yourself!

Many users who discover the PHP magic mathods of __get and __set will find themselves enamoured with them, and attempt to use them in their own code. This is possible, but it's almost always a bad idea.

Yii has an intricate system of housekeeping that supports almost anything you wish to accomplish on your own - especially Virtual Attributes - and attempting to circumvent this may provide more smoke than light. It will almost certainly make your application harder to understand.

If you must override the magic methods in your own code, be sure to call parent::___get($attr) (et al) to give Yii a crack at the attributes in case your code doesn't handle it.

Please treat these methods as highly advanced, only to be used with good reason and careful consideration.

PHP Dynamic Attributes Don't Work

More advanced PHP developers might wonder how Dynamic Attributes play into Yii, and the short answer is that they do not.

Dynamic Attributes allow an object variable to receive new attributes just by the using: saying $object->foo = 1 automatically adds the attribute "foo" to the object without having to resort to __get or __set, and it's reported to be much faster.

But because of Yii's implementation of __get/set, these will not work because the low-level methods in CComponent throw an exception for an unknown attribute rather than let it fall through (which would enable dynamic attributes).

Some question the wisdom of blocking this, though others may well appreciate the safety it provides by insuring that a typo in an attribute name won't silently do the wrong thing rather than attempt to assign a close-but-not-quite attribute name to an object.

More info: Dynamic Properties in PHP