Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Magic getter/setter not being called

Tags:

oop

php

echo

When I was reading the OOP chapter in the Zend PHP Certification study guide, 5.5, I found a question that gave me a shock from its answer. This question is:

class Magic
{
    public $a = "A";
    protected $b = array( "a" => "A" , "b" => "B" , "c" => "C" );
    protected $c = array( 1 , 2 , 3 );

    public function __get( $v )
    {
        echo "$v";
        return $this->b[$v];
    }

    public function __set( $var , $val )
    {
        echo "$var: $val,";
        $this->$var = $val;
    }
}

$m = new Magic();
echo $m->a . ", " . $m->b . ", " . $m->c . ", ";
$m->c = "CC";
echo $m->a . ", " . $m->b . ", " . $m->c . ", ";

The output for this code is:

b, c, A, B, C, c: CC, b, c, A, B, C

Why does this code not print a, and how does it work?

like image 551
Abutouq Avatar asked Nov 22 '14 12:11

Abutouq


2 Answers

__get is only invoked for non-existent or invisible properties. In other words, when you write

$obj->prop

if prop is defined and is visible in the current context, it will be returned "as is", without calling __get.

Example:

class X {

    public   $pub = 1;
    private  $pri = 2;

    function __get($v) {
        echo "[get $v]\n";
        return 42;
    }

    function test() {
        echo $this->foo, "\n";  // __get invoked
        echo $this->pri, "\n";  // no __get
        echo $this->pub, "\n";  // no __get

    }
}

$x = new X;
$x->test();

echo $x->foo, "\n"; // __get invoked
echo $x->pri, "\n"; // __get invoked (property not visible)
echo $x->pub, "\n"; // no __get

This explains why magic->a doesn't invoke the getter. Now, since you also have the setter defined, magic->c = CC actually changes the protected member of the class, therefore, when you echo magic->c later on, this still invokes the getter (due to c's invisibility), and the getter returns this->b[c], not the actual value of this->c.

Here's your code, slightly rewritten for clarity:

class Magic
{
    public $a = "publicA";
    protected $values = array( "a" => "valA" , "b" => "valB" , "c" => "valC" );
    protected $c = "oldC";

    public function __get( $v )
    {
        echo "[get $v]\n";
        return $this->values[$v];
    }

    public function __set( $var , $val )
    {
        echo "[set $var=$val]\n";
        $this->$var = $val;
    }
}

$m = new Magic();
echo $m->a . ", " . $m->b . ", " . $m->c . "\n";
$m->c = "newC";
echo $m->a . ", " . $m->b . ", " . $m->c . "\n";

Results:

[get b]      
[get c]
publicA, valB, valC  # no getter for `a` 
[set c=newC]
[get b]
[get c]              # getter still invoked for `c`
publicA, valB, valC  # no getter for `a`

An exercise to the reader:

Why is the output different if you replace dots . in the echo statements with commas:

$m = new Magic();
echo $m->a , ", " , $m->b , ", " , $m->c , "\n";
$m->c = "newC";
echo $m->a . ", " , $m->b , ", " , $m->c , "\n";
like image 159
georg Avatar answered Oct 21 '22 07:10

georg


Actually, here's what gets printed (demo):

bcA, B, C, c: CC,bcA, B, C,

The point is, each part of $m->a . ", " . $m->b . ", " . $m->c . ", " expression is evaluated BEFORE the result will be printed by echo. In this case, there's a side effect of such evaluation.

As @georg demonstrated, that would have been rather different if the code were written as echo $m->a, ', ', $m->b, ', ', $m->c, ', ' instead: in that case, each operand would have been sent to output immediately after its evaluation!

$m->a part is easy to evaluate, as a is a public property; its value is string 'A'.

$m->b part, however, is a tricky one: b is a protected property, hence magic method __get() is to be called. This method prints b (the name of the property accessed) and returns B (value of $this->b['b']). That's why you see your line starting with b - it's printed before the whole expression is evaluated!

The same happens with $m->c, as c is also a protected property: the getter itsels prints c, but returns C (value of $this->b['c']), which will be printed as part of the whole expression.

After that $m->c = "CC" line is processed, calling another - setter - magic method (as c is protected, remember). This method is what prints c: CC,, as $var is equal to the name of the property to be set, and $val is its new value (passed into __set).

But even though magic setter eventually changes the value of c property, the next line in the code will output the same string as before. It's because the magic getter never access $this->c - it returns the value of $this->b['c'] instead, when queried for c.


The bottom line is: also the certification guides and various similar-flavoured tests love magic methods, in the real code it's better avoid them unless you really know what you're going to get. ) Usually these things are hidden deep within the frameworks' cores, serving as engines for high-level abstraction; it's very rare that you need to provide yet another level of high-level abstraction over that one.

like image 23
raina77ow Avatar answered Oct 21 '22 07:10

raina77ow