95

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

idmean
  • 14,540
  • 9
  • 54
  • 83
airbear
  • 969
  • 1
  • 7
  • 4

8 Answers8

185

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

Gordon
  • 312,688
  • 75
  • 539
  • 559
  • 8
    To elaborate on this, just remove "public $bar" so the property does not exist any more and it will work like a charm. – Steffen Jan 17 '11 at 13:37
  • Thanks. That's what I get for not thoroughly reading the manual and instead looking at some guys blog example of it ;) Still waiting for true operator overloading in PHP, though. – airbear Jan 18 '11 at 06:05
  • @airbear there is an [old PECL package by Sara Golemon that would allow you to overload operators](http://pecl.php.net/package/operator). Don't know how compatible to PHP.current it is though. – Gordon Jan 18 '11 at 07:54
  • @Gordon how to handle properties which are not exists? when I want to set value to them, it simply set. – Pooya Dec 25 '14 at 11:10
  • @Pooya I dont understand your question. When your class implements `__set()` calling `$foo->doesNotExist = 42` will do whatever you defined in `__set()`. – Gordon Dec 25 '14 at 12:32
  • actually no. __set() method is not trigger at all. – Pooya Dec 25 '14 at 20:57
  • tnx, you are right, but still not working for multilevel property call: $foo->doesNotExist->node = 42; – Pooya Dec 26 '14 at 10:11
  • 1
    @Pooya That's because `node` is not a property of $foo but a property of `doesNotExist`. So, unless 'doesNotExist' is an object (which implements __set or has a public property called node) it won't work. – Tivie Dec 27 '14 at 07:18
  • could use the __set to add a __set function to nonexisting values to keep the functionality. – Sarfaraaz Jun 18 '15 at 06:47
  • By definition of _inaccessible_, does it also cover _undefined properties_? – Allen Linatoc Jan 13 '16 at 14:14
  • 1
    @Allen yes, it does. – Gordon Jan 13 '16 at 14:15
  • I just want to add a note, if you are using `__GET` to access a private property, this `empty($object->property)` will always evaluate `true` even if the property contains a value, rather you must use `empty($object->__GET('property')` – Chad May 29 '18 at 18:23
  • @Chad - can you give an example of that happening? Since you are coding `__GET` yourself, its entirely dependent on the code you write. If `$name` is not one of your private properties, then `return null;`. If it *is* one of your properties, then `empty` will follow the exact same rules it always done. As a test, make the property `public`. Still get `empty`? Then the problem isn't `__GET`. – ToolmakerSteve Apr 12 '19 at 21:05
  • @Gordon I disagree with your statement "The magic methods are not substitutes for getters and setters. ". They are substitutes if your properties are marked as private or protected. In my opinion this shoud be always the case to ensure the data encapsulation which are the main goal of OOP. However there are rare cases where a property has to be public. But then the developer should have a dammed good reason to to so. Any public property can change to whatever at runtime (< Version 8) and therefore are a possibility for errors at runtime – Alexander Behling Sep 24 '21 at 08:33
  • @Gordon In most cases the value of a property is set at runtime from a user input via GET/POST or from the database. A user input is always a string. he same applies to values from the DB. Therefor assigning this value to a property will result in changing it type to string when php version < 7.4 see https://www.php.net/manual/en/language.types.declarations.php. Personally I define all properties in my classes as private / protected and use the __get / __set function to do checks and throw specific exceptions as necessary. That's how OOP should work. – Alexander Behling Sep 24 '21 at 08:47
  • @AlexanderBehling Disagreeing is fine. But please ask yourself why you want to shoehorn and cram all property access in two magic methods instead of writing dedicated accessors for the individual properties? If you need getters and setters, write getters and setters `¯\_(ツ)_/¯` – Gordon Sep 24 '21 at 10:22
  • @Gordon You ask why. There are two reasons for this: 1. data encapsulation, as required by the OOP concept and which I have already mentioned. 2. normal allocations/reads without the need to explicitly call a getX / setX method. 3. less code to write I hope you aware that a getter / setter is only needed if the property isn't defined public. So you the question you have to ask yourself is "Why does this property has to be public?". If defined public there is no need to call a method to manipulate the value of this property. Before PHP 7.4 this was a big source of errors. – Alexander Behling Sep 27 '21 at 06:56
  • @AlexanderBehling data encapsulation is perfectly possible without magic methods. there is nothing wrong with calling a getter or setter. by funneling all accessors through magic methods you are making your code harder to read and having higher cyclomatic complexity. – Gordon Sep 27 '21 at 08:15
46

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.

kaiser
  • 21,817
  • 17
  • 90
  • 110
Fidi
  • 5,754
  • 1
  • 18
  • 25
22

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.

Berry Langerak
  • 18,561
  • 4
  • 45
  • 58
9

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.

Community
  • 1
  • 1
Jason Ensinger
  • 101
  • 1
  • 2
  • I found a problem on the 'access' => $property->getModifier() – macki Jan 13 '16 at 00:32
  • +1 for _"in the grand scheme of things, even 5 times slower is never actually slow"_ - readability (among other important things) should never be compromised for premature optimization. Someone who encounters an actual speed issue with `__get()` should probably know better in order to not use it in the first place. – Jay Dadhania Sep 01 '22 at 07:58
6

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.

Matt Lowden
  • 2,586
  • 17
  • 19
2

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
Antoine
  • 800
  • 3
  • 14
  • 29
DevWL
  • 17,345
  • 6
  • 90
  • 86
0

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

Alix Axel
  • 151,645
  • 95
  • 393
  • 500
-7

Intenta con:

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

_SET($k,$v){
 return $this->$k = $v;
}
vezunchik
  • 3,669
  • 3
  • 16
  • 25
charly
  • 1