开发者

PHP __get and __set magic methods

Unless I'm completely mistaken, the __get and __set methods are supposed to allow overloading of the → get and set.

For example, the following statements should invoke the __get method:

echo $foo->bar;
$var = $foo->bar;

And the following should use the __set method:

$foo->bar = 'test';

This was not working in my code, and is reproducible with this simple example:

class foo {

    public $bar;
    public function __get($name) {

        echo "Get:$name";
        return $this->$name;
    }

    public function __set($name, $value) {

        echo "Set:$name to $value";
        $this->$name = $value;
    }
}


$foo = new foo();

echo $foo->bar;
$foo开发者_如何学Go->bar = 'test';

echo "[$foo->bar]";

This only results in:

[test]

Putting some die() calls in there shows that it is not hitting it at all.

For now, I just said screw it, and am manually using __get where it's needed for now, but that's not very dynamic and requires knowledge that the 'overloaded' code is in fact not being called unless specifically called. I'd like to know if this is either not supposed to function the way I've understood that it should or why this is not working.

This is running on php 5.3.3.


__get, __set, __call and __callStatic are invoked when the method or property is inaccessible. Your $bar is public and therefor not inaccessible.

See the section on Property Overloading in the manual:

  • __set() is run when writing data to inaccessible properties.
  • __get() is utilized for reading data from inaccessible properties.

The magic methods are not substitutes for getters and setters. They just allow you to handle method calls or property access that would otherwise result in an error. As such, there are much more related to error handling. Also note that they are considerably slower than using proper getter and setter or direct method calls.


I'd recommend to use an array for storing all values via __set().

class foo {

    protected $values = array();

    public function __get( $key )
    {
        return $this->values[ $key ];
    }

    public function __set( $key, $value )
    {
        $this->values[ $key ] = $value;
    }

}

This way you make sure, that you can't access the variables in another way (note that $values is protected), to avoid collisions.


From the PHP manual:

  • __set() is run when writing data to inaccessible properties.
  • __get() is utilized for reading data from inaccessible properties.

This is only called on reading/writing inaccessible properties. Your property however is public, which means it is accessible. Changing the access modifier to protected solves the issue.


To expand on Berry's answer, that setting the access level to protected allows __get and __set to be used with explicitly declared properties (when accessed outside the class, at least) and the speed being considerably slower, I'll quote a comment from another question on this topic and make a case for using it anyway:

I agree that __get is more slow to a custom get function (doing the same things), this is 0.0124455 the time for __get() and this 0.0024445 is for custom get() after 10000 loops. – Melsi Nov 23 '12 at 22:32 Best practice: PHP Magic Methods __set and __get

According to Melsi's tests, considerably slower is about 5 times slower. That is definitely considerably slower, but also note that the tests show that you can still access a property with this method 10,000 times, counting time for loop iteration, in roughly 1/100 of a second. It is considerably slower in comparison with actual get and set methods defined, and that is an understatement, but in the grand scheme of things, even 5 times slower is never actually slow.

The computing time of the operation is still negligible and not worth considering in 99% of real world applications. The only time it should really be avoided is when you're actually going to be accessing the properties over 10,000 times in a single request. High traffic sites are doing something really wrong if they can't afford throwing a few more servers up to keep their applications running. A single line text ad on the footer of a high traffic site where the access rate becomes an issue could probably pay for a farm of 1,000 servers with that line of text. The end user is never going to be tapping their fingers wondering what is taking the page so long to load because your application's property access takes a millionth of a second.

I say this speaking as a developer coming from a background in .NET, but invisible get and set methods to the consumer is not .NET's invention. They simply aren't properties without them, and these magic methods are PHP's developer's saving grace for even calling their version of properties "properties" at all. Also, the Visual Studio extension for PHP does support intellisense with protected properties, with that trick in mind, I'd think. I would think with enough developers using the magic __get and __set methods in this way, the PHP developers would tune up the execution time to cater to the developer community.

Edit: In theory, protected properties seemed like it'd work in most situation. In practice, it turns out that there's a lot of times you're going to want to use your getters and setters when accessing properties within the class definition and extended classes. A better solution is a base class and interface for when extending other classes, so you can just copy the few lines of code from the base class into the implementing class. I'm doing a bit more with my project's base class, so I don't have an interface to provide right now, but here is the untested stripped down class definition with magic property getting and setting using reflection to remove and move the properties to a protected array:

/** Base class with magic property __get() and __set() support for defined properties. */
class Component {
    /** Gets the properties of the class stored after removing the original
     * definitions to trigger magic __get() and __set() methods when accessed. */
    protected $properties = array();

    /** Provides property get support. Add a case for the property name to
     * expand (no break;) or replace (break;) the default get method. When
     * overriding, call parent::__get($name) first and return if not null,
     * then be sure to check that the property is in the overriding class
     * before doing anything, and to implement the default get routine. */
    public function __get($name) {
        $caller = array_shift(debug_backtrace());
        $max_access = ReflectionProperty::IS_PUBLIC;
        if (is_subclass_of($caller['class'], get_class($this)))
            $max_access = ReflectionProperty::IS_PROTECTED;
        if ($caller['class'] == get_class($this))
            $max_access = ReflectionProperty::IS_PRIVATE;
        if (!empty($this->properties[$name])
            && $this->properties[$name]->class == get_class()
            && $this->properties[$name]->access <= $max_access)
            switch ($name) {
                default:
                    return $this->properties[$name]->value;
            }
    }

    /** Provides property set support. Add a case for the property name to
     * expand (no break;) or replace (break;) the default set method. When
     * overriding, call parent::__set($name, $value) first, then be sure to
     * check that the property is in the overriding class before doing anything,
     * and to implement the default set routine. */
    public function __set($name, $value) {
        $caller = array_shift(debug_backtrace());
        $max_access = ReflectionProperty::IS_PUBLIC;
        if (is_subclass_of($caller['class'], get_class($this)))
            $max_access = ReflectionProperty::IS_PROTECTED;
        if ($caller['class'] == get_class($this))
            $max_access = ReflectionProperty::IS_PRIVATE;
        if (!empty($this->properties[$name])
            && $this->properties[$name]->class == get_class()
            && $this->properties[$name]->access <= $max_access)
            switch ($name) {
                default:
                    $this->properties[$name]->value = $value;
            }
    }

    /** Constructor for the Component. Call first when overriding. */
    function __construct() {
        // Removing and moving properties to $properties property for magic
        // __get() and __set() support.
        $reflected_class = new ReflectionClass($this);
        $properties = array();
        foreach ($reflected_class->getProperties() as $property) {
            if ($property->isStatic()) { continue; }
            $properties[$property->name] = (object)array(
                'name' => $property->name, 'value' => $property->value
                , 'access' => $property->getModifier(), 'class' => get_class($this));
            unset($this->{$property->name}); }
        $this->properties = $properties;
    }
}

My apologies if there are any bugs in the code.


It's because $bar is a public property.

$foo->bar = 'test';

There is no need to call the magic method when running the above.

Deleting public $bar; from your class should correct this.


Best use magic set/get methods with predefined custom set/get Methods as in example below. This way you can combine best of two worlds. In terms of speed I agree that they are a bit slower but can you even feel the difference. Example below also validate the data array against predefined setters.

"The magic methods are not substitutes for getters and setters. They just allow you to handle method calls or property access that would otherwise result in an error."

This is why we should use both.

CLASS ITEM EXAMPLE

    /*
    * Item class
    */
class Item{
    private $data = array();

    function __construct($options=""){ //set default to none
        $this->setNewDataClass($options); //calling function
    }

    private function setNewDataClass($options){
        foreach ($options as $key => $value) {
            $method = 'set'.ucfirst($key); //capitalize first letter of the key to preserve camel case convention naming
            if(is_callable(array($this, $method))){  //use seters setMethod() to set value for this data[key];      
                $this->$method($value); //execute the setters function
            }else{
                $this->data[$key] = $value; //create new set data[key] = value without seeters;
            }   
        }
    }

    private function setNameOfTheItem($value){ // no filter
        $this->data['name'] = strtoupper($value); //assign the value
        return $this->data['name']; // return the value - optional
    }

    private function setWeight($value){ //use some kind of filter
        if($value >= "100"){ 
            $value = "this item is too heavy - sorry - exceeded weight of maximum 99 kg [setters filter]";
        }
        $this->data['weight'] = strtoupper($value); //asign the value
        return $this->data['weight']; // return the value - optional
    }

    function __set($key, $value){
        $method = 'set'.ucfirst($key); //capitalize first letter of the key to preserv camell case convention naming
        if(is_callable(array($this, $method))){  //use seters setMethod() to set value for this data[key];      
            $this->$method($value); //execute the seeter function
        }else{
            $this->data[$key] = $value; //create new set data[key] = value without seeters;
        }
    }

    function __get($key){
        return $this->data[$key];
    }

    function dump(){
        var_dump($this);
    }
}

INDEX.PHP

$data = array(
    'nameOfTheItem' => 'tv',
    'weight' => '1000',
    'size' => '10x20x30'
);

$item = new Item($data);
$item->dump();

$item->somethingThatDoNotExists = 0; // this key (key, value) will trigger magic function __set() without any control or check of the input,
$item->weight = 99; // this key will trigger predefined setter function of a class - setWeight($value) - value is valid,
$item->dump();

$item->weight = 111; // this key will trigger predefined setter function of a class - setWeight($value) - value invalid - will generate warning.
$item->dump(); // display object info

OUTPUT

object(Item)[1]
  private 'data' => 
    array (size=3)
      'name' => string 'TV' (length=2)
      'weight' => string 'THIS ITEM IS TOO HEAVY - SORRY - EXIDED WEIGHT OF MAXIMUM 99 KG [SETTERS FILTER]' (length=80)
      'size' => string '10x20x30' (length=8)
object(Item)[1]
  private 'data' => 
    array (size=4)
      'name' => string 'TV' (length=2)
      'weight' => string '99' (length=2)
      'size' => string '10x20x30' (length=8)
      'somethingThatDoNotExists' => int 0
object(Item)[1]
  private 'data' => 
    array (size=4)
      'name' => string 'TV' (length=2)
      'weight' => string 'THIS ITEM IS TOO HEAVY - SORRY - EXIDED WEIGHT OF MAXIMUM 99 KG [SETTERS FILTER]' (length=80)
      'size' => string '10x20x30' (length=8)
      'somethingThatDoNotExists' => int 0


Drop the public $bar; declaration and it should work as expected.


Intenta con:

__GET($k){
 return $this->$k;
}

_SET($k,$v){
 return $this->$k = $v;
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜