Yii 1.1: Understanding Virtual Attributes and get/set methods

50 followers

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:

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

Total 19 comments

#15843 report it
PrplHaz4 at 2013/12/22 02:10am
@larrytx

Very surprised returning "yes" is not working. Your hyperlink call is wrong though....I think it should be:

return CHtml::link($this->url_alt, $this->url, array('target' => '_blank'));
#15839 report it
LarryTX at 2013/12/21 01:43pm
Can't get virtual attributes to work

I've tried desperately to get getters to work can't seem to do it. I'm hoping that someone can help. Here's what I've got thus far.

In the model:

public function getHyperlink() {
return CHtml($this->url_alt, $this->url, array('target' => '_blank'));
}

In the rules() for the model:

array('notes, directions, description, hyperlink', 'safe'),

In my view (where I have commented out everything else for testing purposes):

<?php
    echo $model->hyperlink . '<br />';
    echo $model->url . '<br />';
    echo $model->url_alt . '<br />';
    echo $model->name;
?>

Taken together, all of that displays:

http://library.ndsu.edu/grhc/history_culture/textile/olgastolz.html
North Dakota State University - Germans from Russia Heritage Collection - 10-point Poinsettia
10-point Poinsettia

As you can see, the model attributes name, url, and url_alt are all retrieved and displayed properly. The virtual attribute hyperlink is simply returned null.

I can't for the life of me figure out what I'm doing wrong. I even changed the return statement in the getter to read return "Yes", and it still returns null. It seems as if Yii is totally ignoring the getter. Any ideas?

#13927 report it
elexperimento at 2013/07/06 04:54pm
GREAT ARTICLE

Man, at last I understand the concept of magic methods, and the virtual attributes in yii. MANY THANKS and best regards.

#13116 report it
Sankalp Singha at 2013/05/06 03:53am
Thank you so much!

This article is a a very important article and thank you so much for writing about it and saving me and countless others from hours of headache. I had huge doubts about the virtual methods which are cleared now thanks to this article.

Regards,

#12198 report it
engvard at 2013/03/05 05:17pm
Dynamic attributes

I needed dynamic attributes very-very badly so I implemented them with overriding __set, __get (these are obvious ones) and setAttributes to make validators work. Here's the function:

public function setAttributes($attributes, $safe = true) {
        foreach ($attributes as $attribute=>$value) {
            if (property_exists( get_class ($this), $attribute)) {
                $this->$attribute = $value;
            } else {
                $this->dynamic_attributes[$attribute] = $value;
            }
        }
    }
#9425 report it
meadows at 2012/08/09 06:22pm
Love these, but is there anyway to get the filter working?

I've tried a few things that haven't worked...is there anyway to get the filter to work with these on a CGridview? I would be happy if I could even make it so it only filtered on 1 field if it could stay in the inline filter row.

#7543 report it
Daniel at 2012/03/29 03:34am
Re: model->attributes empty ??

Hi oceatoon,

I encountered this problem, too. But, I tried to put the virtual attributes in the rule, either as required or safe depends on the your need. In my case, the $model->attributes was working.

Hope this can help.

#7412 report it
Tibor Katelbach at 2012/03/21 12:09pm
model->attributes empty ??

I'm a bit confused about this article (nevertheless extremely interesting) it Yii wont build virtual attributes and some comments say it works ??

in my case when building my model dynamically , $model-attributes is empty ? I'm on a CFormModel where if I build the attributes of the class manuelly (written in the class), it all runs fine. but when using __construct, __get/__set

function __construct($data){
      foreach ($myClassList as $key) {
          $this->$key = $data[$key];
      }
      parent::__construct();
    }
public function __set($key, $value){$this->$key = $value;}
public function __get($value){ return $value;}
#5160 report it
The Peach at 2011/09/20 12:20pm
NOTE

Please note that you don't need to define the attribute as public in the model, as it would create a mismatch and the __set/__get won't be looking for the setter/getter of the attribute because of a check done with attribute_exists.

Hope this can save some headache to someone.

Wrote an addendum article a while ago on the subject, for anyone interested:

How Yii virtual attributes work

#3579 report it
Maurizio Domba Cerin at 2011/04/20 04:06am
Re: Are virtual attributes in $this->attributes ?

To be assigned by using $this->attributes... they should be "safe"... ie. have some rules defined...

#3574 report it
Matthew at 2011/04/19 05:48pm
Are virtual attributes in $this->attributes ?

Form have some table-related fields and some other used to save dynamic fields (PostgreSQL arrays). I'm using virtual attributes to do it, but they don't show up in $this->attributes, making necessary to manually assign them. Shouldn't they be in $this->attributes so they can be auto-assigned?

#3391 report it
Antonio Ramirez at 2011/04/08 05:00pm
virtual properties

@rudiedirkx

One solution to the problem is that you create your own component extending from CComponent and then, override the __get, __isset, and __set, functions before and after calling parent CComponent functions respectively.

But I am wondering, if I allow any non-previously set properties, wouldn't it be too much 'error prone'? How would you check for non-declared attributes as the correct ones? What would be the validation for those? Isn't that StdClass existing already for that?

Please, apologies if I didn't really understand what you express.

Great article, good comments contribution

Cheers

#3307 report it
Dudie Rirkx at 2011/04/01 11:46am
dynamic object properties

Excellent addition to the page about PHP and Yii's dynamic properties!

#3305 report it
Steve Friedl at 2011/04/01 10:45am
Dynamic Attributes

This is an interesting feature of PHP, and I've added a section to the article explaining that it doesn't work, and this ought to provide a heads up to an experienced developer wondering why it didn't work the way it expects.

But I don't understand the purpose of showing an example that is known not to work in a wiki article intended to help people - it's just going to confuse people.

Wiki comments are not the vehicle for debating whether Yii ought to do something or not, but feel free to make your own article if you wish to elaborate on the point.

#3300 report it
abajja at 2011/04/01 06:42am
dynamic object properties

Just to clarify some concepts, here it is a link: http://krisjordan.com/dynamic-properties-in-php-with-stdclass

#3299 report it
Dudie Rirkx at 2011/04/01 05:43am
dynamic object properties

My solution doesn't work (I knew that) because Yii doesn't allow dynamic object properties (which is incredibly stupid if you ask me). Defining the property beforehand would fix that, but break the magic Yii attribute retrieval process.

So there's no 'solution' for this? Yii never allows undefined properties to be set?

In the case of firstname + lastname there is indeed no performance loss/gain, but you can imagine much much much more can be done in a dynamic attribute method.

Also @SJFriedl, my caching 'mechanism' is actually native PHP, which makes the whole thing even faster (__get is never executed after the first time!).

#3293 report it
Steve Friedl at 2011/03/31 05:31pm
Performance overconcern

It's true that one can pay attention to performance issues by doing some caching, but for something simple like first+last name, the overhead of the caching mechanism will be greater than the actual time adding the strings together. Remember that these aren't cached for any long term, only for the duration of one request, so it's not always worth it.

Nevertheless, it's a good point to keep in mind for more complicated virtual functions.

But this:

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

does not work at all; try it.

#3280 report it
thomas.mery at 2011/03/31 03:52am
@rudiedirkx - performance

you have to code the storage of the result of the getter function yourself

what is usually done is declaring a private variable that will store the value obtained through your getter function logic

you start your getter function by checking if this private variable is set and return it if it is, thus bypassing the virtual attribute retrieval process

I think this porcess might be a good addition to this nice article.

class Person extends CActiveRecord {
 
   private var $_fullname;
 
   public function getFullName()
   {
 
      if(isset($this->_fullname)) {
        return $this->_fullname;
      }
 
      $this->_fullname = $this->firstname . " " . $this->lastname;
 
      return $this->_fullname;
 
   }
 
}
#3279 report it
Dudie Rirkx at 2011/03/31 03:18am
performance

What happens if you call ->fullname twice or thrice or a dozen times? 1) The whole thing is repeated and repeated or 2) is the path to the ->getFullname() stored somewhere or 3) the ->getFullname() result is cached into ->fullname.

There's a very big big difference. I hope it's never 1) and that it's configurable to be 2) OR 3) (per virtual attribute!!).

Is there any documentation for this kind of inner workings?

Leave a comment

Please to leave your comment.

Write new article