PHP - Indirect modification of overloaded property

PhpMagic Methods

Php Problem Overview


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...

Php Solutions


Solution 1 - Php

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.

Solution 2 - Php

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 ;
	}	
}

Solution 3 - Php

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'

Solution 4 - Php

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.

Solution 5 - Php

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);

Solution 6 - Php

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...

  1. 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! :)

Solution 7 - Php

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.

Solution 8 - Php

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";

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionw00View Question on Stackoverflow
Solution 1 - PhpVinnyDView Answer on Stackoverflow
Solution 2 - PhpBabaView Answer on Stackoverflow
Solution 3 - PhpPhil WView Answer on Stackoverflow
Solution 4 - PhpAlthaf MView Answer on Stackoverflow
Solution 5 - PhpAndrewView Answer on Stackoverflow
Solution 6 - PhpShoresView Answer on Stackoverflow
Solution 7 - PhpKlaas van der WeijView Answer on Stackoverflow
Solution 8 - PhpNickView Answer on Stackoverflow