In using PHP's DOM classes (DOMNode, DOMEElement, etc) I have noticed that they possess truly readonly properties. For example, I can read the $nodeName property of a DOMNode, but I cannot write to it (if I do PHP throws a fatal error).
How can I create readonly properties of my own in PHP?
You can do it like this:
class Example {
private $__readOnly = 'hello world';
function __get($name) {
if($name === 'readOnly')
return $this->__readOnly;
user_error("Invalid property: " . __CLASS__ . "->$name");
}
function __set($name, $value) {
user_error("Can't set property: " . __CLASS__ . "->$name");
}
}
Only use this when you really need it - it is slower than normal property access. For PHP, it's best to adopt a policy of only using setter methods to change a property from the outside.
Since PHP 8.1 there are implemented native readonly properties
Documentation
You can initialize readonly property only once during the declaration of the property.
class Test {
public readonly string $prop;
public function __construct(string $prop) {
$this->prop = $prop;
}
}
--
class Test {
public function __construct(
public readonly string $prop,
) {}
}
Trying to modify the readonly propety will cause following error:
Error: Cannot modify readonly property Test::$prop
Update PHP 8.2
Since PHP 8.2 you are able to define as readonly a whole class.
readonly class Test {
public string $prop;
public function __construct(string $prop) {
$this->prop = $prop;
}
}
But private properties exposed only using __get() aren't visible to functions that enumerate an object's members - json_encode() for example.
I regularly pass PHP objects to Javascript using json_encode() as it seems to be a good way to pass complex structures with lots of data populated from a database. I have to use public properties in these objects so that this data is populated through to the Javascript that uses it, but this means that those properties have to be public (and therefore run the risk that another programmer not on the same wavelength (or probably myself after a bad night) might modify them directly). If I make them private and use __get() and __set(), then json_encode() doesn't see them.
Wouldn't it be nice to have a "readonly" accessibility keyword?
Here is a way to render all property of your class read_only from outside, inherited class have write access ;-).
class Test {
protected $foo;
protected $bar;
public function __construct($foo, $bar) {
$this->foo = $foo;
$this->bar = $bar;
}
/**
* All property accessible from outside but readonly
* if property does not exist return null
*
* #param string $name
*
* #return mixed|null
*/
public function __get ($name) {
return $this->$name ?? null;
}
/**
* __set trap, property not writeable
*
* #param string $name
* #param mixed $value
*
* #return mixed
*/
function __set ($name, $value) {
return $value;
}
}
tested in php7
I see you have already got your answer but for the ones who still are looking:
Just declare all "readonly" variables as private or protected and use the magic method __get() like this:
/**
* This is used to fetch readonly variables, you can not read the registry
* instance reference through here.
*
* #param string $var
* #return bool|string|array
*/
public function __get($var)
{
return ($var != "instance" && isset($this->$var)) ? $this->$var : false;
}
As you can see I have also protected the $this->instance variable as this method will allow users to read all declared variabled. To block several variables use an array with in_array().
For those looking for a way of exposing your private/protected properties for serialization, if you choose to use a getter method to make them readonly, here is a way of doing this (#Matt: for json as an example):
interface json_serialize {
public function json_encode( $asJson = true );
public function json_decode( $value );
}
class test implements json_serialize {
public $obj = null;
protected $num = 123;
protected $string = 'string';
protected $vars = array( 'array', 'array' );
// getter
public function __get( $name ) {
return( $this->$name );
}
// json_decode
public function json_encode( $asJson = true ) {
$result = array();
foreach( $this as $key => $value )
if( is_object( $value ) ) {
if( $value instanceof json_serialize )
$result[$key] = $value->json_encode( false );
else
trigger_error( 'Object not encoded: ' . get_class( $this ).'::'.$key, E_USER_WARNING );
} else
$result[$key] = $value;
return( $asJson ? json_encode( $result ) : $result );
}
// json_encode
public function json_decode( $value ) {
$json = json_decode( $value, true );
foreach( $json as $key => $value ) {
// recursively loop through each variable reset them
}
}
}
$test = new test();
$test->obj = new test();
echo $test->string;
echo $test->json_encode();
Class PropertyExample {
private $m_value;
public function Value() {
$args = func_get_args();
return $this->getSet($this->m_value, $args);
}
protected function _getSet(&$property, $args){
switch (sizeOf($args)){
case 0:
return $property;
case 1:
$property = $args[0];
break;
default:
$backtrace = debug_backtrace();
throw new Exception($backtrace[2]['function'] . ' accepts either 0 or 1 parameters');
}
}
}
This is how I deal with getting/setting my properties, if you want to make Value() readonly ... then you simply just have it do the following instead:
return $this->m_value;
Where as the function Value() right now would either get or set.
Related
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.
I'm making a BaseModel class and want to use magic methods __set and __get instead of defining setters and getters for every single property.
I'm currently using variable variables because I couldn't find another way by googling it. Are variable variables considered bad practice or am I fretting over nothing?
abstract class BaseModel implements \ArrayAccess {
/**
* Don't allow these member variables to be written by __set
*
* #var array
*/
protected $noSet = array();
/**
* Don't allow these member variables to be retrieved by __get
*
* #var array
*/
protected $noGet = array();
public function offsetExists( $offset )
{
return property_exists($this, $offset);
}
public function offsetGet( $offset )
{
return $this->__get($offset);
}
public function offsetSet( $offset , $value )
{
return $this->__set($offset, $value);
}
public function offsetUnset( $offset )
{
unset($this->$offset);
}
public function __get($member)
{
if( $member == 'noSet' || $member == 'noGet')
{
throw new \InvalidArgumentException ("Tried to access a forbidden property", 1);
}
if( ! property_exists($this, $member))
{
throw new \InvalidArgumentException ("Tried to access a non-existent property", 1);
}
if( in_array($member, $this->noGet))
{
throw new \InvalidArgumentException ("Tried to access a forbidden property", 1);
}
return $this->$member;
}
public function __set($member, $value)
{
if( $member == 'noSet' || $member == 'noGet')
{
throw new \DomainException ("Tried write to a non-writable property.", 1);
}
if( ! property_exists($this, $member))
{
throw new \InvalidArgumentException ("Tried to access a non-existent property", 1);
}
if( in_array($member, $this->noSet))
{
throw new \DomainException ("Tried write to a non-writable property.", 1);
}
return $this->$member = $value;
}
First, it seems you think that protected keyword makes a property unable to be set/get using magic methods. This is not the case. This just makes it to where you can't directly access/modify these properties from outside the scope of the class (i.e. you can't do something like $object->foo = 'bar')
Second, you seem to have misunderstanding of magic methods. What they in fact do is to enforce behavior when a user tries to directly access/modify a property. So in my example, if a user tries to do:
$object->foo = 'bar';
This is actually calls the __set() method and is equivalent to:
$object->__set('foo', 'bar');
So a typical class implementation using get/set magic methods might look like this:
class some_class {
protected $foo;
protected $foo2;
public $pub;
public function __construct() {
// maybe do something here
}
public function __get($prop) {
if(!property_exists($this, $prop) {
throw new Exception('Tried to get unknown property ' . $prop);
} else {
return $this->{$prop};
}
}
public function __set($prop, $value) {
if(!property_exists($this, $prop) {
throw new Exception('Tried to set unknown property ' . $prop);
} else {
$this->{$prop} = $value;
return true; // or whatever you want to return
}
}
}
Usage would be as follows:
$object = new some_class();
$object->foo = 'bar'; // sets 'bar'
echo $object->foo; // echo 'bar;
var_dump($object->foo2); // null
$object->pub = 'something'; // does not call __set() as this property is available from global scope
echo $object->pub; // echo 'something' does not call __get() as this property is available from global scope
$object->no_prop; // throws Exception from __get() as property does not exist
It would seem to be odd usage to try to actually call __get() or __set() from within the class.
Check out the PHP documentation on object overloading for more information:
http://www.php.net/manual/en/language.oop5.overloading.php#object.get
Variable variables are indeed the way to go.
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.
I was writing a class that uses __get() and __set() to store and retrieve array elements in a master array. I had a check to make some elements ungettable, basically to re-create private properties.
I noticed that it seemed that __get intercepts all calls to class properties. This sucks for me, because I wanted to have a variable private to the outside world ( unavailable via get ), but I was trying to access it by directly referencing the master array from within the class. Of course, the master array is not in the whitelist of gettable properties :(
Is there a way I can emulate public and private properties in a php class that uses __get() and __set()?
Example:
<?
abstract class abstraction {
private $arrSettables;
private $arrGettables;
private $arrPropertyValues;
private $arrProperties;
private $blnExists = FALSE;
public function __construct( $arrPropertyValues, $arrSettables, $arrGettables ) {
$this->arrProperties = array_keys($arrPropertyValues);
$this->arrPropertyValues = $arrPropertyValues;
$this->arrSettables = $arrSettables;
$this->arrGettables = $arrGettables;
}
public function __get( $var ) {
echo "__get()ing:\n";
if ( ! in_array($var, $this->arrGettables) ) {
throw new Exception("$var is not accessible.");
}
return $this->arrPropertyValues[$var];
}
public function __set( $val, $var ) {
echo "__set()ing:\n";
if ( ! in_array($this->arrSettables, $var) ) {
throw new Exception("$var is not settable.");
}
return $this->arrPropertyValues[$var];
}
} // end class declaration
class concrete extends abstraction {
public function __construct( $arrPropertyValues, $arrSettables, $arrGettables ) {
parent::__construct( $arrPropertyValues, $arrSettables, $arrGettables );
}
public function runTest() {
echo "Accessing array directly:\n";
$this->arrPropertyValues['color'] = "red";
echo "Color is {$this->arrPropertyValues['color']}.\n";
echo "Referencing property:\n";
echo "Color is {$this->color}.\n";
$this->color = "blue";
echo "Color is {$this->color}.\n";
$rand = "a" . mt_rand(0,10000000);
$this->$rand = "Here is a random value";
echo "'$rand' is {$this->$rand}.\n";
}
}
try {
$objBlock = & new concrete( array("color"=>"green"), array("color"), array("color") );
$objBlock->runTest();
} catch ( exception $e ) {
echo "Caught Exeption $e./n/n";
}
// no terminating delimiter
$ php test.php
Accessing array directly:
__get()ing:
Caught Exeption exception 'Exception' with message 'arrPropertyValues is not accessible.' in /var/www/test.php:23
Stack trace:
#0 /var/www/test.php(50): abstraction->__get('arrPropertyValu...')
#1 /var/www//test.php(68): concrete->runTest()
#2 {main}.
Is there a way I can emulate public and private properties in a php class that uses __get() and __set()?
Not directly (if you discount debug_backtrace).
But you can have a private method getPriv that does all the work your current __get does. Then __get would only wrap this private method and check accessibility.
function __get($name) {
if (in_array($name, $this->privateProperties))
throw new Exception("The property ". __CLASS__ . "::$name is private.");
return $this->getPriv($name);
}
Inside your class, you would call getPriv, thus bypassing __get.
Make abstraction::$arrPropertyValues protected or do what Artefacto wrote (if you need additional checks), except that abstraction::getPriv() should be protected.
Rather than manually enlisting private/protected properties, you could use PHPs cumbersome reflection methods:
function __get($name) {
$reflect = new ReflectionObject($this);
$publics = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
if (in_array($name, $publics)) {
return $this->{$name};
}
}
I have a class where I'm using __set. Because I don't want it to set just anything, I have an array of approved variables that it checks before it will actually set a class property.
However, on construct, I want the __construct method to set several class properties, some of which are not in the approved list. So when construct happens, and I do $this->var = $value, I of course get my exception that I'm not allowed to set that variable.
Can I get around this somehow?
Declare the class members:
class Blah
{
private $imAllowedToExist; // no exception thrown because __set() wont be called
}
Declaring the class members is your best bet. If that doesn't work, you could have a switch ($this->isInConstructor?) which determines whether to throw the error.
On the other hand, you could also use the __get method as well as the __set method and have both of them map to a wrapped library:
class Foo
{
private $library;
private $trustedValues;
public function __construct( array $values )
{
$this->trustedValues = array( 'foo', 'bar', 'baz' );
$this->library = new stdClass();
foreach( $values as $key=>$value )
{
$this->library->$key = $value;
}
}
public function __get( $key )
{
return $this->library->$key;
}
public function __set( $key, $value )
{
if( in_array( $key, $this->trustedValues ) )
{
$this->library->$key = $value;
}
else
{
throw new Exception( "I don't understand $key => $value." );
}
}
}