- An Easy Virtual Attribute
- Getters and Setters in Detail
- Resolving Conflicts
- Do not use get and set yourself!
- 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:
- 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. - Database Fields (in CActiveRecord)
- Database Relations (in CActiveRecord)
- Virtual Attributes defined with get/set functions (in CComponent)
- 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
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?
@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; } }
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.
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!).
dynamic object properties
Just to clarify some concepts, here it is a link:
http://krisjordan.com/dynamic-properties-in-php-with-stdclass
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.
dynamic object properties
Excellent addition to the page about PHP and Yii's dynamic properties!
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
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?
Re: Are virtual attributes in $this->attributes ?
To be assigned by using $this->attributes... they should be "safe"... ie. have some rules defined...
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
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;}
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.
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.
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; } } }
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,
GREAT ARTICLE
Man, at last I understand the concept of magic methods, and the virtual attributes in yii. MANY THANKS and best regards.
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?
@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'));
virtual properties
Very helpfull article.
Thanks
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.