78

I know this question has been asked several times, but none of them have a real answer for a workaround. Maybe there's one for my specific case.

I'm building a mapper class which uses the magic method __get() to lazy load other objects. It looks something like this:

public function __get ( $index )
{
    if ( isset ($this->vars[$index]) )
    {
        return $this->vars[$index];
    }

    // $index = 'role';
    $obj = $this->createNewObject ( $index );

    return $obj;
}

In my code I do:

$user = createObject('user');
$user->role->rolename;

This works so far. The User object doesn't have a property called 'role', so it uses the magic __get() method to create that object and it returns its property from the 'role' object.

But when i try to modify the 'rolename':

$user = createUser();
$user->role->rolename = 'Test';

Then it gives me the following error:

Notice: Indirect modification of overloaded property has no effect

Not sure if this is still some bug in PHP or if it's "expected behaviour", but in any case it doesn't work the way I want. This is really a show stopper for me... Because how on earth am I able to change the properties of the lazy loaded objects??


EDIT:

The actual problem only seems to occur when I return an array which contains multiple objects.

I've added an example piece of code which reproduces the problem:

http://codepad.org/T1iPZm9t

You should really run this in your PHP environment the really see the 'error'. But there is something really interesting going on here.

I try to change the property of an object, which gives me the notice 'cant change overloaded property'. But if I echo the property after that I see that it actually DID change the value... Really weird...

Andrew
  • 18,680
  • 13
  • 103
  • 118
w00
  • 26,172
  • 30
  • 101
  • 147
  • Your theory is correct and it is indeed possible. This code works fine and demonstrates your desired functionality http://codepad.org/jklKQpu2. Which means there's something else happening in your code. Is it possible to provide a small, reproducible case? – Mike B May 04 '12 at 19:33
  • @MikeB It's hard to create a good example. I'm using database with PDO::FETCH_CLASS. That's actually what creates the object for me. – w00 May 04 '12 at 19:51
  • @MikeB I've created an example of my problem. You don't need a Database. The problem is, is that i return an array which has objects. Take a look at this code: http://codepad.org/0fsgfemn --- It's better though to run this locally. Codepad doesn't show the 'cant modify overloaded property' error – w00 May 04 '12 at 20:11
  • Logically, the PHP intrinsic overload functionality should take care of this; gawd why does PHP have to be so damn buggy, c'mon guys, really? please fix this. –  Jul 06 '18 at 02:57
  • 1
    @argon This is not a bug, this is the way it works, and it is very kind of PHP to provde this warning. He is returning an array which is not passed by reference in php. – Joey Novak Jul 26 '18 at 20:03

8 Answers8

109

All you need to do is add "&" in front of your __get function to pass it as reference:

public function &__get ( $index )

Struggled with this one for a while.

VinnyD
  • 3,500
  • 9
  • 34
  • 48
  • 4
    Can anyone help with an explanation why this works? What does the & symbol before variables or functions even do? – Greg L Nov 17 '14 at 19:21
  • 10
    It causes the function to return by reference instead of by value - meaning that you can update the value of the property. If it just returned by value (ie. without an ampersand), any change you make to that value will not be stored in the property. – Russ Dec 04 '14 at 11:50
  • 3
    This is the most expected answer that I was looking for! – Kerem Dec 21 '14 at 19:50
  • 2
    This works, but you can't throw exceptions and return NULL or false value's anymore. only value's by reference. – Nijboer IT May 18 '17 at 13:41
  • This should be the accepted answer. In case your `__get` must return `null`, just set a return variable to `null` and return that variable. –  Jul 06 '18 at 12:58
  • This should be the ONE answer for the presented issue. Simple, straight forward and on-target with the question. THANK YOU so much for sharing. Saved me tons of hours debugging and considering possibilities. It makes so much sense now that I stumbled upon that (and the answer) that I even feel silly... :) You Rock @VinnyD – Julio Marchi Sep 07 '19 at 15:55
16

Nice you gave me something to play around with

Run

class Sample extends Creator {

}

$a = new Sample ();
$a->role->rolename = 'test';
echo  $a->role->rolename , PHP_EOL;
$a->role->rolename->am->love->php = 'w00';
echo  $a->role->rolename  , PHP_EOL;
echo  $a->role->rolename->am->love->php   , PHP_EOL;

Output

test
test
w00

Class Used

abstract class Creator {
    public function __get($name) {
        if (! isset ( $this->{$name} )) {
            $this->{$name} = new Value ( $name, null );
        }
        return $this->{$name};
    }

    public function __set($name, $value) {
        $this->{$name} = new Value ( $name, $value );
    }



}

class Value extends Creator {
    private $name;
    private $value;
    function __construct($name, $value) {
        $this->name = $name;
        $this->value = $value;
    }

    function __toString()
    {
        return (string) $this->value ;
    }
}      

Edit : New Array Support as requested

class Sample extends Creator {

}

$a = new Sample ();
$a->role = array (
        "A",
        "B",
        "C" 
);


$a->role[0]->nice = "OK" ;

print ($a->role[0]->nice  . PHP_EOL);

$a->role[1]->nice->ok = array("foo","bar","die");

print ($a->role[1]->nice->ok[2]  . PHP_EOL);


$a->role[2]->nice->raw = new stdClass();
$a->role[2]->nice->raw->name = "baba" ;

print ($a->role[2]->nice->raw->name. PHP_EOL);

Output

 Ok die baba

Modified Class

abstract class Creator {
    public function __get($name) {
        if (! isset ( $this->{$name} )) {
            $this->{$name} = new Value ( $name, null );
        }
        return $this->{$name};
    }

    public function __set($name, $value) {
        if (is_array ( $value )) {
            array_walk ( $value, function (&$item, $key) {
                $item = new Value ( $key, $item );
            } );
        }
        $this->{$name} = $value;

    }

}

class Value {
    private $name ;
    function __construct($name, $value) {
        $this->{$name} = $value;
        $this->name = $value ;
    }

    public function __get($name) {
        if (! isset ( $this->{$name} )) {
            $this->{$name} = new Value ( $name, null );
        }

        if ($name == $this->name) {
            return $this->value;
        }

        return $this->{$name};
    }

    public function __set($name, $value) {
        if (is_array ( $value )) {
            array_walk ( $value, function (&$item, $key) {
                $item = new Value ( $key, $item );
            } );
        }
        $this->{$name} = $value;
    }

    public function __toString() {
        return (string) $this->name ;
    }   
}
Baba
  • 94,024
  • 28
  • 166
  • 217
  • The problem is, is that i return an array which has objects. That's causing the problem. Take a look at this code: http://codepad.org/0fsgfemn --- It's better though to run this locally. Codepad doesn't show the 'cant modify overloaded property' error – w00 May 04 '12 at 20:11
  • array was not initially included in your question ... i would take a look at it an create a patch .. – Baba May 04 '12 at 20:14
  • You're right. I didn't know the array was actually causing the problem. I edited my opening post because i found something interesting... – w00 May 04 '12 at 20:20
  • How would you be able to iterate over the values stored through this method? – VinnyD Nov 01 '12 at 20:35
  • Yes .. all you need to is implement `RecursiveIterator` ..... and implement `getName()` and `getValue()` in your value class ; – Baba Nov 01 '12 at 21:04
  • To be noted that another possible fix is simply defining `public function & __get($name) {...}`, which works too! Thanks for this though, it saved my day :) – Ocramius Jun 29 '13 at 13:11
  • @Baba I just saw this and it's amazing, but I have one question. Is it possible to make the Value class act as an array? Take a look at this: http://pastebin.com/V3G2CE6v – alexandernst Apr 06 '15 at 16:47
  • @alexandernst yes its possible :) – Baba Jul 27 '15 at 11:08
  • @Baba Can you write an example, please? – alexandernst Jul 27 '15 at 11:15
10

I've had this same error, without your whole code it is difficult to pinpoint exactly how to fix it but it is caused by not having a __set function.

The way that I have gotten around it in the past is I have done things like this:

$user = createUser();
$role = $user->role;
$role->rolename = 'Test';

now if you do this:

echo $user->role->rolename;

you should see 'Test'

Phil W
  • 549
  • 2
  • 5
  • Thank you! this concept worked for me, have tried others all over SO, but this finally helped. Legend! – Haring10 Mar 06 '16 at 16:05
4

Though I am very late in this discussion, I thought this may be useful for some one in future.

I had faced similar situation. The easiest workaround for those who doesn't mind unsetting and resetting the variable is to do so. I am pretty sure the reason why this is not working is clear from the other answers and from the php.net manual. The simplest workaround worked for me is

Assumption:

  1. $object is the object with overloaded __get and __set from the base class, which I am not in the freedom to modify.
  2. shippingData is the array I want to modify a field of for e.g. :- phone_number

 

// First store the array in a local variable.
$tempShippingData = $object->shippingData;

unset($object->shippingData);

$tempShippingData['phone_number'] = '888-666-0000' // what ever the value you want to set

$object->shippingData = $tempShippingData; // this will again call the __set and set the array variable

unset($tempShippingData);

Note: this solution is one of the quick workaround possible to solve the problem and get the variable copied. If the array is too humungous, it may be good to force rewrite the __get method to return a reference rather expensive copying of big arrays.

DarthJDG
  • 16,511
  • 11
  • 49
  • 56
Althaf M
  • 498
  • 4
  • 12
3

I was receiving this notice for doing this:

$var = reset($myClass->my_magic_property);

This fixed it:

$tmp = $myClass->my_magic_property;
$var = reset($tmp);
Andrew
  • 18,680
  • 13
  • 103
  • 118
3

I agree with VinnyD that what you need to do is add "&" in front of your __get function, as to make it to return the needed result as a reference:

public function &__get ( $propertyname )

But be aware of two things:

1) You should also do

return &$something;

or you might still be returning a value and not a reference...

2) Remember that in any case that __get returns a reference this also means that the corresponding __set will NEVER be called; this is because php resolves this by using the reference returned by __get, which is called instead!

So:

$var = $object->NonExistentArrayProperty; 

means __get is called and, since __get has &__get and return &$something, $var is now, as intended, a reference to the overloaded property...

$object->NonExistentArrayProperty = array(); 

works as expected and __set is called as expected...

But:

$object->NonExistentArrayProperty[] = $value;

or

$object->NonExistentArrayProperty["index"] = $value;

works as expected in the sense that the element will be correctly added or modified in the overloaded array property, BUT __set WILL NOT BE CALLED: __get will be called instead!

These two calls would NOT work if not using &__get and return &$something, but while they do work in this way, they NEVER call __set, but always call __get.

This is why I decided to return a reference

return &$something;

when $something is an array(), or when the overloaded property has no special setter method, and instead return a value

return $something;

when $something is NOT an array or has a special setter function.

In any case, this was quite tricky to understand properly for me! :)

Shores
  • 95
  • 7
  • Thank you for the additional insight. I was considering the same when I start testing it and you covered the scope very well. Now, back to test scenarios... :) I will let you guys know if I find any issues with the final implementation. – Julio Marchi Sep 07 '19 at 15:58
  • An additional comment: when I am to return a value that exists in the Object (from a Property), it is OK. But, if I try to return anything else directly, the same issue resurfaces. A simple example is when trying to return TRUE or FALSE. The solution was either created a Property in the Object with the value to be returned, and then return its value (I.e.: ```private $value = false;``` then use ```return $this->value;``` instead of ```return false;```) or simple add the specific content to be returned in the ```$this->value``` before returning it. No more issues. – Julio Marchi Sep 07 '19 at 20:26
  • I think that the issue you talk about in your last comment is also not a real issue, but a direct consequence of the facts I showed in the answer: since TRUE or FALSE are NOT variables, but values, they CANNOT be passed by reference. When you put the value in an object property, and then return the reference to the property, all works properly again, since, as you can see, an object property is a variable, and so can get passed by reference. – Shores Sep 09 '19 at 12:44
  • That is a great explanation. Thank you! In my case, I wrapped the code from `__get()` in a `try {} catch($e) {} finally {}` statement. Based on the case you described, it allowed me to return a specifically created Method Property with the correct value I needed to return. That even allowed me to remove the `&` from the `__get()` Magic Method (not actually needed). By tracing the code with a step-by-step debug I understood how `__get()` is called from other Magic Methods that deal with the Object Data (`__isset()`, `__unset(),` etc.), so I could successfully treat the condition. – Julio Marchi Sep 10 '19 at 18:48
2

This is occurring due to how PHP treats overloaded properties in that they are not modifiable or passed by reference.

See the manual for more information regarding overloading.

To work around this problem you can either use a __set function or create a createObject method.

Below is a __get and __set that provides a workaround to a similar situation to yours, you can simply modify the __set to suite your needs.

Note the __get never actually returns a variable. and rather once you have set a variable in your object it no longer is overloaded.

/**
 * Get a variable in the event.
 *
 * @param  mixed  $key  Variable name.
 *
 * @return  mixed|null
 */
public function __get($key)
{
    throw new \LogicException(sprintf(
        "Call to undefined event property %s",
        $key
    ));
}

/**
 * Set a variable in the event.
 *
 * @param  string  $key  Name of variable
 *
 * @param  mixed  $value  Value to variable
 *
 * @return  boolean  True
 */
public function __set($key, $value)
{
    if (stripos($key, '_') === 0 && isset($this->$key)) {
        throw new \LogicException(sprintf(
            "%s is a read-only event property", 
            $key
        ));
    }
    $this->$key = $value;
    return true;
}

Which will allow for:

$object = new obj();
$object->a = array();
$object->a[] = "b";
$object->v = new obj();
$object->v->a = "b";
Nick
  • 763
  • 1
  • 11
  • 26
  • The problem is, is that i return an array which has objects. That's causing the problem. Take a look at this code: http://codepad.org/0fsgfemn --- It's better though to run this locally. Codepad doesn't show the 'cant modify overloaded property' error – w00 May 04 '12 at 20:11
  • *passed by value. if they were passed by reference, there would be no issue at all :( – Wes Aug 06 '15 at 17:04
2

I have run into the same problem as w00, but I didn't had the freedom to rewrite the base functionality of the component in which this problem (E_NOTICE) occured. I've been able to fix the issue using an ArrayObject in stead of the basic type array(). This will return an object, which will defaulty be returned by reference.

Klaas van der Weij
  • 1,065
  • 10
  • 13