Difference between #5 and #4 of Understanding Virtual Attributes and get/set methods

unchanged
Title
Understanding Virtual Attributes and get/set methods
unchanged
Category
Tutorials
unchanged
Tags
Virtual Attributes; __get; __set
changed
Content
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:
~~~
[php]
class Comment extends CActiveRecord {
    public $helperVariable;
    public function rules() { ... }
    ...
}
~~~
and then use them in the obvious way:
~~~
[php]
$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:
~~~
[php]
  $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:
~~~
[php]
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:
~~~
[php]
// 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:
~~~
[php]
   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):
~~~
[php]
$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 referneces,references, any function
beginning with "set" or "get" can generally be used as an
attribute.>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.