PHP Visibility with Magic Methods __get & __set - php

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

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

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.

Related

PHP magic methods behave differently when dynamically building property names

I have two examples of a simple class using the __set() and __get() magic methods. One throws a fatal error and the other does not when attempting to access a protected property with the unset() function.
In Example 1, I'm naming my protected property starting with an underscore and allowing access via the friendly name and prepending the underscore in my __set() and __get() methods. (Effectively exposing the property without an underscore).
In Example 2, I'm not starting the name with an underscore and allowing access via the name directly in the __set() and __get() methods.
Questions
1) Why does Example 1 not throw a fatal error while Example 2 does throw a fatal error? I would expect either both to throw the error or neither to throw the error.
2) Also, why does Example 1 not actually unset the property? I would expect the property to not contain a value after the unset() function is called.
Example 1
class Example {
protected $_my_property;
function __get($name) {
echo '<h4>__get() was triggered!</h4>';
$name = '_' . $name;
if (property_exists($this, $name)) {
return $this->$name;
}
else {
trigger_error("Undefined property in __get(): $name");
return NULL;
}
}
function __set($name, $value) {
echo '<h4>__set() was triggered!</h4>';
$name = '_' . $name;
if (property_exists($this, $name)) {
$this->$name = $value;
return;
}
else {
trigger_error("Undefined property in __set(): {$name}");
}
}
}
$myExample = new Example();
$myExample->my_property = 'my_property now has a value';
echo $myExample->my_property;
unset($myExample->my_property);
echo "Did I unset my property?: {$myExample->my_property}";
Example 2
class Example {
protected $my_property;
function __get($name) {
echo '<h4>__get() was triggered!</h4>';
if (property_exists($this, $name)) {
return $this->$name;
}
else {
trigger_error("Undefined property in __get(): $name");
return NULL;
}
}
function __set($name, $value) {
echo '<h4>__set() was triggered!</h4>';
if (property_exists($this, $name)) {
$this->$name = $value;
return;
}
else {
trigger_error("Undefined property in __set(): {$name}");
}
}
}
$myExample = new Example();
$myExample->my_property = 'my_property now has a value';
echo $myExample->my_property;
unset($myExample->my_property);
echo "Did I unset my property?: {$myExample->my_property}";
As a side note, this is just a simplistic example that demonstrates the behavior I'm seeing in my real world project. Thanks!
The problem you have is that you have not defined an __unset() magic method.
This means that when you call unset($myExample->my_property), it is trying to directly unset a public property with the specified name.
In example 1, the real protected property has an underscore in it's name. Therefore, when you try to unset the property, PHP looks at the object, sees that there is nothing with the specified name, and effectively ignores it.
This is the same behaviour that unset() would exhibit if you tried to unset a non-existent variable or array element.
However in example 2, the protected property has the same name as you have given in the unset() call.
In this example, PHP looks at the object and sees that the property does exist but that it it is non-accessible. It therefore throws an error complaining that it can't unset the property.
You can resolve this issue by including an __unset() method alongside your __get() and __set() methods. If you're planning to use magic methods, you should ideally define all three.
Hope that helps.

PHP overloading - avoid Notice: Trying to get property of non-object

I'm using __get and __set methods for variable overloading.
In some cases I will assign another object to a variable, for example:
$object->name->another = $another_object;
$object, has the __get and __set methods. When I try to access $object->name->another before it's been set, I get the error
Notice: Trying to get property of non-object
Is there anyway around this using overloading? without having to check isset on the variable.
Thanks!
While this doesn't answer your question, it addresses an important aspect:
It's worth noting that you ought not allow your magic __get and __set methods to raise that notice. The "best practice" is to throw one of the new(ish) SPL Exception types specifically designed for this purpose. For example:
public function __get($name)
{
if (isset($this->vals[$name])) {
return $this->vals[$name];
} else {
$msg = "Invalid property: '$name' does not exist";
throw new OutOfBoundsException($msg);
}
}
And for __set ...
public function __set($name, $val)
{
if (isset($this->vals[$name])) {
$this->vals[$name] = $val;
} else {
$msg = "Invalid property: '$name' does not exist";
throw new OutOfBoundsException($msg);
}
}
Here's a link to a helpful article on the subject of the new SPL Exception types.
Not $object but $object->name needs to have the overloading with __get()/__set() if you want to interact to prevent the error in question:
$object->name->another = 'something';
^^^^^^^^^^^^^
For example you can make $object::__get() return an empty stdClass object, PHP would then automatically assign a public member to it in this case.
Just make sure you are returning an overloaded class for "name". PHP will fetch the property, then try to set the second property on the resulting object.
<?php
class Bob {
public function __get($name) {
return new Jim();
}
}
class Jim extends Bob {
public function __get($name) {
return 12;
}
}
$b = new Bob();
echo $b->someJim->apples;

How to reuse an object if it existed

I'm trying to determine whether or not a given object has been created. I see there are methods for class_exists and method_exists but what I'm trying to figure out is if new Foo() has been called (and hopefully figure out what variable it was assigned to, but that is not as important).
If I understand you correctly you are trying to initialize object only once. If this is the case why not to use singleton pattern? This will free you from checking of existence of object:
class MyClass {
private static $instance;
private function __construct() {}
public static function getInstance() {
if (empty(self::$instance)) {
self::$instance = new __CLASS__();
}
return self::$instance;
}
}
You can use this code like this:
$obj = MyClass::getInstance();
With similar approach you can define additional helper static methods which will check whether object was instantiated or not. You just need to keep instance statically inside your class.
Edit: After seeing the reason you are needing this in the comments above, this is definitely not the way to go about it.
Here ya go. It could be optimized a little, but should work fine.
Also, passing get_defined_vars() to the function every time is necessary because that function only retrieves the vars within the scope it's called. Calling it inside the function would only give the vars within the scope of that function.
<?php
function isClassDeclared($class_name, $vars, $return_var_name = FALSE) {
foreach ($vars AS $name => $val) {
if (is_object($val) && $val instanceof $class_name)
return $return_var_name ? $name : TRUE;
}
return FALSE;
}
class Foo {}
$foo = new Foo;
echo '<pre>';
var_dump(isClassDeclared('foo', get_defined_vars(), TRUE));
var_dump(isClassDeclared('bar', get_defined_vars(), TRUE));
echo '</pre>';

How to avoid PHP to auto-create attributes?

When doing this :
class MyClass
{
public $myAttr;
}
$c = new MyClass();
$c->someStuff = 1;
I have no error message, no notice, no whatever to tell me "hey, I do not know someStuff !"
Is there anyway to make PHP less easy about this ?
Thanks
Implement the magic methods __get() and __set() and tell them to spit a notice. These methods are called when you access a property that wasn't declared (or isn't visible).
public function __get($name) {
trigger_error(__CLASS__ . " object has no such property $name", E_USER_NOTICE);
return NULL;
}
public function __set($name, $value) {
trigger_error(__CLASS__ . " object has no such property $name", E_USER_NOTICE);
}
I often define a SafeObject to inherit from, which dies when I attempt to read/write an undefined attribute:
class SafeObject {
function __get($key) { die("Attempt to get nonexistent member $key"); }
function __set($key, $value) { die("Attempt to set nonexistent member $key"); }
}
The idea being I should never actually call either of these methods during production code.

Magic Method __set() on a Instantiated Object

Ok i have a problem, sorry if i cant explaint it clear but the code speaks for its self.
i have a class which generates objects from a given class name;
Say we say the class is Modules:
public function name($name)
{
$this->includeModule($name);
try
{
$module = new ReflectionClass($name);
$instance = $module->isInstantiable() ? $module->newInstance() : "Err";
$this->addDelegate($instance);
}
catch(Exception $e)
{
Modules::Name("Logger")->log($e->getMessage());
}
return $this;
}
The AddDelegate Method:
protected function addDelegate($delegate)
{
$this->aDelegates[] = $delegate;
}
The __call Method
public function __call($methodName, $parameters)
{
$delegated = false;
foreach ($this->aDelegates as $delegate)
{
if(class_exists(get_class($delegate)))
{
if(method_exists($delegate,$methodName))
{
$method = new ReflectionMethod(get_class($delegate), $methodName);
$function = array($delegate, $methodName);
return call_user_func_array($function, $parameters);
}
}
}
The __get Method
public function __get($property)
{
foreach($this->aDelegates as $delegate)
{
if ($delegate->$property !== false)
{
return $delegate->$property;
}
}
}
All this works fine expect the function __set
public function __set($property,$value)
{
//print_r($this->aDelegates);
foreach($this->aDelegates as $k=>$delegate)
{
//print_r($k);
//print_r($delegate);
if (property_exists($delegate, $property))
{
$delegate->$property = $value;
}
}
//$this->addDelegate($delegate);
print_r($this->aDelegates);
}
class tester
{
public function __set($name,$value)
{
self::$module->name(self::$name)->__set($name,$value);
}
}
Module::test("logger")->log("test"); // this logs, it works
echo Module::test("logger")->path; //prints /home/bla/test/ this is also correct
But i cant set any value to class log like this
Module::tester("logger")->path ="/home/bla/test/log/";
The path property of class logger is public so its not a problem of protected or private property access.
How can i solve this issue? I hope i could explain my problem clear.
EDIT:
A simple demonstration
Modules::Name("XML_Helper")->xmlVersion ="Hello"; // default is 333
$a = Modules::Name("XML_Helper")->xmlVersion; // now $a should contain "Hello"
echo $a; // prints 333
What i need is
Modules::Name("XML_Helper")->xmlVersion ="Hello"; // default is 333
$a = Modules::Name("XML_Helper")->xmlVersion; // now $a should contain "Hello"
echo $a; // prints Hello
I realise you already said that path is public, but it's still worth mentioning: If you're using PHP 5.3.0+, note this quirk of property_exists():
5.3.0 | This function checks the existence of a property independent of
accessibility
In other words, if you check if (property_exists($delegate, $property)), you have no guarantee you have access to $delegate->$property for writing (or reading, for that matter, but you are trying to write).
As for actual troubleshooting: You could try checking if your if (property_exists($delegate, $property)) statement actually executes. If it doesn't, check the case of $property.
Sidenote: It's fairly hard to read the code you posted up, which makes it a bit of a pain to troubleshoot. Could you edit your post and indent it properly?
The path property of class logger is public so its not a problem of
protected or private property access.
That's your problem. From the docs:
__set() is run when writing data to inaccessible properties.
That suggests that __set() is not called for public properties.

Categories