简体   繁体   中英

PHP Visibility with Magic Methods __get & __set

I recently went to an interview and my code I supplied had magic functions to get and set variables. My code was as follows:

public function __get($name){
    try { 
        return $this->$name;
    } catch (Exception $e) { 
        throw new Exception('Trying to get a variable "'.$name.'" that does not exist.'); 
    }
}

In the interview the guy asked me about the visibility on my variables, I had private ones set but these were now accessible by using magic functions. Essentially I failed the interview on this point, so I wanted to understand more. I was following a tutorial from PHP Master and found a different __get , I have tried to break it but it seems to work, but in a strange way.

I call __get('test') to get my variable _test but if it is set to private it calls itself again and tells me that it cannot access __test . I do not really understand why it calls itself again.

public function __get($name)
{
    $field = '_' . strtolower($name);

    if (!property_exists($this, $field)){
        throw new \InvalidArgumentException(
            "Getting the field '$field' is not valid for this entity"
        );
    }

    $accessor = 'get' . ucfirst(strtolower($name));
    return (method_exists($this, $accessor) && is_callable(array($this, $accessor))) ?
        $this->$accessor() : $this->$field;

}

Can anyone give me some pointers on proper use of __get and __set when using visibility in a class and why this function would call itself again.

I have read the other posts here but I am still struggling with this concept.

I just bumped into this question and there is a little thing that may be worth clarifying:

I do not really understand why it calls itself again.

The code is not calling itself again but trying to execute a custom getter if there is one defined. Let me break down the method execution:

public function __get($name)
{

As already explained in the other answers and here , the __get() magic method is called when you are trying to access a property that is not declared or not visible in the calling scope.

$field = '_' . strtolower($name);

if (!property_exists($this, $field)){
    throw new \InvalidArgumentException(
        "Getting the field '$field' is not valid for this entity"
    );
}

Here it just checks that a property with an underscore pre-appended exists in the class definition. If it doesn't, an exception is thrown.

$accessor = 'get' . ucfirst(strtolower($name));

Here it creates the name of the getter to call if it exists. Thus, if you try to access a property named email and there is a private member called _email the $accessor variable will now hold the 'getEmail' string.

return (method_exists($this, $accessor) && is_callable(array($this, $accessor))) ?
    $this->$accessor() : $this->$field;

The final part is a bit cryiptic, since many things are happening in one line:

  • method_exists($this, $accessor) . Checks if the receiver ( $this ) has a method with $accessor name (in our example, getEmail ).
  • is_callable(array($this, $accessor)) . Checks that the getter can be called .
  • If both conditions are met, the custom getter is called and its return value is returned ( $this->$accessor() ). If not, the property contents are returned ( $this->$field ).

As an example consider this class definition:

class AccessorsExample
{
private $_test1 = "One";
private $_test2 = "Two";

public function getTest2()
{
echo "Calling the getter\n";
return $this->_test2;
}

public function __get($name)
{
    $field = '_' . strtolower($name);

    if (!property_exists($this, $field)){
        throw new \InvalidArgumentException(
            "Getting the field '$field' is not valid for this entity"
        );
    }

    $accessor = 'get' . ucfirst(strtolower($name));
    return (method_exists($this, $accessor) && is_callable(array($this, $accessor))) ?
        $this->$accessor() : $this->$field;

}
}

and then run:

$example = new AccessorsExample();
echo $example->test1 . "\n";
echo $example->test2 . "\n";

You should see:

One
Calling the getter
Two

HTH

I find it's better to be explicit when allowing access to properties via __get() . This way you can still have truly private members, and you don't run the risk of accidentally exposing things you add later.

class Foo
{
  // readonly 
  private $foo;
  private $bar;

  // truly private
  private $baz;

  public function __get($var)
  {
    switch ($var)
    {
      // readonly access to foo and bar, but not baz 
      case 'foo':
      case 'bar':
        return $this->$var;

      // readonly dynamically generated property
      case 'buzz':
        return $this->buzz();


      default: 
        throw new InvalidPropertyException($var);
    }
  }

  public function __isset($var)
  {
    switch ($var)
    {
      // return true for foo, bar and buzz so functions like isset() 
      // and empty() work as expected
      case 'foo':
      case 'bar':
      case 'buzz':
        return true;

      default: 
        return false;
    }
  }

  // dynamic readonly property implementation
  private function buzz()
  {
    // calculate and return something which depends on other private properties
  }
}
public function __get($name){
    try { 
        return $this->$name;
    } catch (Exception $e) { 
        throw new Exception('Trying to get a variable "'.$name.'" that does not exist.'); 
    }
}

A couple of problems with this method as it stands, without seeing the rest of the code:

  • It allows unrestricted public read access to all private and protected properties inside the class. Except in special cases this is usually undesirable as it does defeat the object of class member visibility. As mentioned earlier, access should be restricted, either by checking against an allowed list (as in Rob Agar's answer) or by checking for a defined getter (as in the OP's question).

  • An exception is not normally thrown when accessing an undefined property (unless you have a custom error handler that is set to do so). Accessing an undefined property normally triggers an E_NOTICE, so your method would not trap this. You should first validate that $name does actually exist.

I don't get exactl what is your problem, but for example this code works

<?php

class foo {

private $_test = "my";

public function __get($name)
{
    $field = '_' . strtolower($name);

    if (!property_exists($this, $field)){
        throw new InvalidArgumentException(
            "Getting the field '$field' is not valid for this entity"
        );
    }

    $accessor = 'get' . ucfirst(strtolower($name));
    return (method_exists($this, $accessor) && is_callable(array($this, $accessor))) ?
        $this->$accessor() : $this->$field;

}
}

$foo = new foo();

echo $foo->test;

as you can check here (http://codepad.org/jmkvHiDe).

Magic methods like __get() will be called, when you try to access a private property exactly as they would be called to access a non existing property, of course if you set a property as "private" and then the user can access the variable through a magic method, why set the property as private in the first place?

Instead of $this->$name;

Use something like $this->protected_values[$name];

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM