Override __set magic function in php - php

I'm trying to create a method that will allow me to set properties within a class using the setVal() function, if the user is trying to set the value from outside the class without using the 'forceSet' function then it will throw an exception.
The problem is that its throwing an exception even if the $forceSet is true. If i set the property manually in the class to have private access then everything works fine, but this is not an option as I wish to be able to set various properties in this class dynamically.
class test
{
private $_allowedCols = array('title', 'name', 'surname');
public function __set($n,$v)
{
$this->setVal($n, $v);
}
public function setVal($name, $value, $forceSet=false)
{
if (!$forceSet && !in_array($this->_allowedCols, $name))
{
throw new Exception('cant set value');
}
$this->$name = $value;
}
}
$b = new test;
$b->setVal('blah', 'test', true);
print_r($b);
exit;
What I want to be able to do is set all the values from a $_POST into properties in the object. I want to check against the $_allowedCols to make sure only values I want are being put into the object but sometimes I might want to force values in from the code that aren't in the $_allowedCols.
Any ideas?

The hacks will work but it might be cleaner to use an internal array. Something like:
class test
{
private $data = array();
public function __set($n,$v)
{
if (isset($this->data[$n])) return $this->data[$n] = $v;
throw new Exception('cant set value');
}
public function __get($n)
{
if (isset($this->data[$n])) return $this->data[$n];
throw new Exception('cant retrieve value');
}
public function setVal($name, $value)
{
$this->data[$name] = $value;
}
}
But if you want to stick with your approach then:
class test
{
private $forceFlag = false;
public function __set($name,$value)
{
if ($this->forceFlag) return $this->$name = $value;
throw new Exception('cant set value');
}
public function setVal($name, $value)
{
$this->forceFlag = true;
$this->$name = $value;
$this->forceFlag = false;
}
}

If you look at the stack trace of your exception, you'll notice the call to set __set is being triggered by this line:
$this->$name = $value;
Then in __set, it does $this->setVal($n, $v), which uses the default value of false, and thus throws the exception. To fix this, you can modify your call in __set to be:
$this->setVal($n, $v, true);

With the above code, this line:
$this->$name = $value;
...invokes:
test::__set('blah', 'test');
...because test::$blah is undefined, which in turn invokes:
test::setVal('blah', 'test', false);
A possible, yet not perfect, workaround is this:
public function setVal($name, $value, $forceSet=false)
{
if (!$forceSet && isset($value))
{
throw new Exception('cant set value');
}
$this->$name = null;
$this->$name = $value;
}
Although I'm not sure what the point of your code is.

It looks like you write much code for a functionality PHP offers out of the box:
$b = new test;
$b->blah = 'test';
print_r($b);
You don't need __set for this, nor the setVal(ue) function.
However when you want to control the access, you need to ensure that you're not binding it to members. Instead store it inside of a map as a private member:
class test
{
private $values;
public function __set($n,$v)
{
$this->setVal($n, $v);
}
public function setVal($name, $value, $forceSet=false)
{
if (!$forceSet)
{
throw new Exception('cant set value');
}
$this->values[$name] = $value;
}
}
This ensures, that a member exists that is set, so that __set is not triggered again.

After testing so many options .. the is the one that works the best for me
I chose this because
Use of Exception terminates the entire scripts or one has to catch exception anything time a value is declared
__set and __get can easily be overriding by extending class
Implementation that can be used with multiple class
What to be able to use the Object directly without having to add another getter method
Locking can cause conflict
The script would not change your existing application structure
Can be used with Singleton ..
Code :
abstract class Hashtable
{
final $hashTable = array() ;
final function __set($n,$v)
{
return false ;
}
final function __get($n)
{
return #$this->hashTable[$n] ;
}
final function _set($n, $v)
{
$this->hashTable[$n] = $v ;
}
}
class Test extends Hashtable {} ;
$b = new Test();
$b->_set("bar","foo",true);
$b->_set("hello","world",true);
//$b->setVal("very","bad"); // false
$b->bar = "fail" ;
var_dump($b,$b->bar);
Output
object(Test)[1]
public 'hashTable' =>
array
'bar' => string 'foo' (length=3)
'hello' => string 'world' (length=5)
string 'foo' (length=3)
I hope this helps
Thanks
:)

Related

Why PHP `__invoke` Not Working When Triggered from an Object Property

I wonder whether this is a bug or normal. Let’s say I have a class with some magical functions:
class Foo {
public function __toString() {
return '`__toString` called.';
}
public function __get($key) {
return '`__get(' . $key . ')` called.';
}
public function __invoke($x = "") {
return '`__invoke(' . $x . ')` called.';
}
}
And then create an instance in an object property like this:
$object = (object) [
'foo' => 'bar',
'baz' => new Foo
];
Then test it:
echo $object->baz;
echo $object->baz->qux;
echo $object->baz('%'); // :(
It is broken in the last echo: Call to undefined method stdClass::baz()
Currently, the only solution I can do is to store the __invoke part in a temporary variable and then call that variable as a function like this:
$x = $object->baz;
echo $x('%'); // :)
It works fine when I instantiate the class in an array property:
$array = [
'baz' => new Foo
];
echo $array['baz'];
echo $array['baz']->qux;
echo $array['baz']('%'); // :)
By the way, I need this ability on my object for something related to API:
$foo = (object) ['bar' => new MyClass];
echo $foo->bar; → should trigger __toString
echo $foo->bar->baz; → should trigger __get
echo $foo->bar(); → should trigger __invoke
echo $foo->bar->baz(); → should trigger __call
All of them should return a string.
Can this be done in PHP completely? Thanks.
No can do.
The line in question is simply ambigous, and the error message shows you how ... It is more logical to try to access the baz() method of your $object object.
That's just the context given by the parser when it sees $object->baz()
As already mentioned in the comments, you can remove that ambiguity, help the parser by telling it that $object->baz is itself an expression that needs to be executed first:
($object->baz)('arg');
PHP is also itself a program, and has to know how to execute something before executing it. If it could blindly try every possible "magic" method on every object in a $foo->bar->baz->qux chain, then it wouldn't be able to tell you what the error is when it is encountered - it would just silently crash.
I have solved my problem by detecting the existence of an __invoke method inside the __call method of a class.
class MyStdClass extends stdClass {
protected $data = [];
public function __construct(array $array) {
$this->data = $array;
}
public function __get($key) {
return isset($this->data[$key]) ? $this->data[$key] : null;
}
public function __call($key, $args = []) {
if (isset($this->data[$key])) {
$test = $this->data[$key];
// not an object = not an instance, skip!
if (!is_object($test)) {
return $this->__get($key);
}
if (!empty($args) && get_class($test) && method_exists($test, '__invoke')) {
// or `return $test(...$args)`
return call_user_func([$test, '__invoke'], ...$args);
}
}
return $this->__get($key);
}
public function __set($key, $value = null) {
$this->data[$key] = $value;
}
public function __toString() {
return json_encode($this->data);
}
public function __isset($key) {}
public function __unset($key) {}
}
So, instead of converting the array into object with (object), here I use:
$object = new MyStdClass([
'foo' => 'bar',
'baz' => new Foo
]);

Modifying a PHP array element via magic methods (__get and __set)

I have the following class:
/**
* #property int $barMagic
*/
class Foo
{
public $barNormal;
private $attributes = [];
public function __get($name) {
return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
}
public function __set($name, $value)
{
$this->attributes[$name] = $value;
}
}
As you can see, the $barMagic public property is not defined explicitly, it's accessed via the magic methods.
When setting and then modifying an array element in the normal attribute, it works fine:
$foo = new Foo();
$foo->barNormal = ['baz' => 1];
echo $foo->barNormal['baz'];
$foo->barNormal['baz'] = 2;
echo ',' . $foo->barNormal['baz'];
It outputs "1,2", just as intended.
But when using the magic property, it does not:
$foo = new Foo();
$foo->barMagic = ['baz' => 1];
echo $foo->barMagic['baz'];
$foo->barMagic['baz'] = 2;
echo ',' . $foo->barMagic['baz'];
It outputs "1,1"!
Is there a way in PHP to access array elements in magic properties the same way as normal ones?
The ArrayAccess interface seems to deal with array access one level higher than I need it.
The real answer is tricky and involves some bug/inconsistency in the PHP engine. As commentors suggested, I added the "&" (return by reference) character before __get(). So new code:
public function &__get($name) {
return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
}
but this gives
Notice: Only variable references should be returned by reference in ....
I had to change it to
public function &__get($name) {
if (isset($this->attributes[$name])) {
return $this->attributes[$name];
} else {
return null;
}
}
and now it works. Note that the two snippets should be completely equivalent, but they are not. Thank you all for the contribution, you took me halfway there.

PHP - Indirect modification of overloaded property

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...
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.
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 ;
}
}
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'
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:
$object is the object with overloaded __get and __set from the base class, which I am not in the freedom to modify.
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.
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);
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...
2) 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! :)
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";
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.

read-only properties in PHP?

Is there a way to make a read-only property of an object in PHP? I have an object with a couple arrays in it. I want to access them as I normally would an array
echo $objObject->arrArray[0];
But I don't want to be able to write to those arrays after they're constructed. It feels like a PITA to construct a local variable:
$arrArray = $objObject->getArray1();
echo $arrArray[0];
And anyways, while it keeps the array in the object pristine, it doesn't prevent me from re-writing the local array variable.
Well, the question is where do you want to prevent writing from?
The first step is making the array protected or private to prevent writing from outside of the object scope:
protected $arrArray = array();
If from "outside" of the array, a GETTER will do you fine. Either:
public function getArray() { return $this->arrArray; }
And accessing it like
$array = $obj->getArray();
or
public function __get($name) {
return isset($this->$name) ? $this->$name : null;
}
And accessing it like:
$array = $obj->arrArray;
Notice that they don't return references. So you cannot change the original array from outside the scope of the object. You can change the array itself...
If you really need a fully immutable array, you could use a Object using ArrayAccess...
Or, you could simply extend ArrayObject and overwrite all of the writing methods:
class ImmutableArrayObject extends ArrayObject {
public function append($value) {
throw new LogicException('Attempting to write to an immutable array');
}
public function exchangeArray($input) {
throw new LogicException('Attempting to write to an immutable array');
}
public function offsetSet($index, $newval) {
throw new LogicException('Attempting to write to an immutable array');
}
public function offsetUnset($index) {
throw new LogicException('Attempting to write to an immutable array');
}
}
Then, simply make $this->arrArray an instance of the object:
public function __construct(array $input) {
$this->arrArray = new ImmutableArrayObject($input);
}
It still supports most array like usages:
count($this->arrArray);
echo $this->arrArray[0];
foreach ($this->arrArray as $key => $value) {}
But if you try to write to it, you'll get a LogicException...
Oh, but realize that if you need to write to it, all you need to do (within the object) is do:
$newArray = $this->arrArray->getArrayCopy();
//Edit array here
$this->arrArray = new ImmutableArrayObject($newArray);
If you're using PHP 5+ you can do it with __set() and __get() methods.
You have to define how they work but should do just this.
Edit an example would be like this.
class Example {
private $var;
public function __get($v) {
if (is_array($v)) {
foreach () {
// handle it here
}
} else {
return $this->$v;
}
}
}
This might not be the "best" way of doing it but it'll work depending on what you need
If defined, the magic functions __get() and __set() will be called whenever a non-existing or private property is accessed. This can be used to create "get" and "set" methods for private properties, and for instance make them read-only or manipulate the data when stored or retrieved in it.
For instance:
class Foo
{
private $bar = 0;
public $baz = 4; // Public properties will not be affected by __get() or __set()
public function __get($name)
{
if($name == 'bar')
return $this->bar;
else
return null;
}
public function __set($name, $value)
{
// ignore, since Foo::bar is read-only
}
}
$myobj = new Foo();
echo $foo->bar; // Output is "0"
$foo->bar = 5;
echo $foo->bar; // Output is still "0", since the variable is read-only
See also the manual page for overloading in PHP.
For PHP 8.1+, you can use readonly properties:
class Test
{
public readonly array $arrArray;
public function __construct()
{
$this->arrArray = [1, 2, 3];
}
}
$test = new Test();
var_dump($test->arrArray); // OK
$test->arrArray = [4, 5, 6]; // Error
in the class, do this:
private $array;
function set_array($value) {
$this->array = $value;
}
then you just set like this:
$obj->set_array($new_array);

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