Problem: I am trying to extend PHP's ArrayObject as shown below. Unfortunately I can't get it to work properly when setting multi-dimensional objects and instead an error thrown as I have the strict settings enabled in PHP. (Error: Strict standards: Creating default object from empty value)
Question: How can I modify my class to automatically create non-existing levels for me?
The code:
$config = new Config;
$config->lvl1_0 = true; // Works
$config->lvl1_1->lvl2 = true; // Throws error as "lvl1" isn't set already
class Config extends ArrayObject
{
function __construct() {
parent::__construct(array(), self::ARRAY_AS_PROPS);
}
public function offsetSet($k, $v) {
$v = is_array($v) ? new self($v) : $v;
return parent::offsetSet($k, $v);
}
}
Taking a more oop view of your issue, you can create a class that models the concept of an multi-dimensional object.
The solution im posting doesn't extends from ArrayObject to achieve the goals you mention. As you tagged your question as oop, i think it´s important to reinforce the separation the way you store an object's state from how do you access it.
Hope this will help you achieve what you need!
From what you said, an multi-dimensional object is one that:
handles multiple levels of nested information
it does so by providing reading/writing access to the information via properties
behaves nicely when undefined properties are accessed. This means that, for example, you do the following on an empty instance: $config->database->host = 'localhost' the database and host levels are initialized automatically, and host will return 'localhost' when queried.
ideally, would be initialized from an associative arrays (because you can already parse config files into them)
Proposed Solution
So, how can those features be implemented?
The second one is easy: using PHP's __get and __set methods. Those will get called whenever an read/write is beign done on an inaccessible property (one that's not defined in an object).
The trick will be then not to declare any property and handle propertie's operations through those methods and map the property name being accessed as a key to an associative array used as storage. They'll provide basically an interface for accessing information stored internally.
For the third one, we need a way to create a new nesting level when a undeclared property is read.
The key point here is realizing that the returned value for the property must be an multi-dimensional object so further levels of nesting can be created from it also: whenever we´re asked for a property whose name is not present in the internal array, we´ll associate that name with a new instance of MultiDimensionalObject and return it. The returned object will be able to handle defined or undefined properties too.
When an undeclared property is written, all we have to do is assign it's name with the value provided in the internal array.
The fourth one is easy (see it on __construct implementation). We just have to make sure that we create an MultiDimensionalObject when a property's value is an array.
Finally, the fist one: the way we handle the second and third features allows us to read and write properties (declared and undeclared) in any level of nesting.
You can do things like $config->foo->bar->baz = 'hello' on an empty instance and then query for $config->foo->bar->baz successfully.
Important
Notice that MultiDimensionalObject instead of beign itself an array is it composed with an array, letting you change the way you store the object's state as needed.
Implementation
/* Provides an easy to use interface for reading/writing associative array based information */
/* by exposing properties that represents each key of the array */
class MultiDimensionalObject {
/* Keeps the state of each property */
private $properties;
/* Creates a new MultiDimensionalObject instance initialized with $properties */
public function __construct($properties = array()) {
$this->properties = array();
$this->populate($properties);
}
/* Creates properties for this instance whose names/contents are defined by the keys/values in the $properties associative array */
private function populate($properties) {
foreach($properties as $name => $value) {
$this->create_property($name, $value);
}
}
/* Creates a new property or overrides an existing one using $name as property name and $value as its value */
private function create_property($name, $value) {
$this->properties[$name] = is_array($value) ? $this->create_complex_property($value)
: $this->create_simple_property($value);
}
/* Creates a new complex property. Complex properties are created from arrays and are represented by instances of MultiDimensionalObject */
private function create_complex_property($value = array()){
return new MultiDimensionalObject($value);
}
/* Creates a simple property. Simple properties are the ones that are not arrays: they can be strings, bools, objects, etc. */
private function create_simple_property($value) {
return $value;
}
/* Gets the value of the property named $name */
/* If $name does not exists, it is initilialized with an empty instance of MultiDimensionalObject before returning it */
/* By using this technique, we can initialize nested properties even if the path to them don't exist */
/* I.e.: $config->foo
- property doesn't exists, it is initialized to an instance of MultiDimensionalObject and returned
$config->foo->bar = "hello";
- as explained before, doesn't exists, it is initialized to an instance of MultiDimensionalObject and returned.
- when set to "hello"; bar becomes a string (it is no longer an MultiDimensionalObject instance) */
public function __get($name) {
$this->create_property_if_not_exists($name);
return $this->properties[$name];
}
private function create_property_if_not_exists($name) {
if (array_key_exists($name, $this->properties)) return;
$this->create_property($name, array());
}
public function __set($name, $value) {
$this->create_property($name, $value);
}
}
Demo
Code:
var_dump(new MultiDimensionalObject());
Result:
object(MultiDimensionalObject)[1]
private 'properties' =>
array
empty
Code:
$data = array( 'database' => array ( 'host' => 'localhost' ) );
$config = new MultiDimensionalObject($data);
var_dump($config->database);
Result:
object(MultiDimensionalObject)[2]
private 'properties' =>
array
'host' => string 'localhost' (length=9)
Code:
$config->database->credentials->username = "admin";
$config->database->credentials->password = "pass";
var_dump($config->database->credentials);
Result:
object(MultiDimensionalObject)[3]
private 'properties' =>
array
'username' => string 'admin' (length=5)
'password' => string 'pass' (length=4)
Code:
$config->database->credentials->username;
Result:
admin
Implement the offsetGet method. If you are accessing a non exist property, you can create one as you like.
As you are extend ArrayObject, you should use the array way [] to set or get.
Copied pasted your code and it works fine on my PHP test box (running PHP 5.3.6). It does mention the Strict Standards warning, but it still works as expected. Here's the output from print_r:
Config Object
(
[storage:ArrayObject:private] => Array
(
[lvl1_0] => 1
[lvl1_1] => stdClass Object
(
[lvl2] => 1
)
)
)
It is worth noting that on the PHP docs there is a comment with guidance related to what you're trying to do:
sfinktah at php dot spamtrak dot org 17-Apr-2011 07:27
If you plan to derive your own class from ArrayObject, and wish to maintain complete ArrayObject functionality (such as being able to cast to an array), it is necessary to use ArrayObject's own private property "storage".
Detailed explanation is linked above but, in addition to offsetSet which you have and offsetGet which xdazz mentions, you also must implement offsetExists and offsetUnset. This shouldn't have anything to do with your current error but it is something you should be mindful of.
Update: xdazz' second-half has the answer to your problem. If you access your Config object as an array, it works without any errors:
$config = new Config;
$config[ 'lvl1_0' ] = true;
$config[ 'lvl1_1' ][ 'lvl2' ] = true;
Can you do that or are you restricted to the Object syntax for some reason?
Related
I have a object having some protected property that I want to get and set. The object looks like
Fields_Form_Element_Location Object
(
[helper] => formText
[_allowEmpty:protected] => 1
[_autoInsertNotEmptyValidator:protected] => 1
[_belongsTo:protected] =>
[_description:protected] =>
[_disableLoadDefaultDecorators:protected] =>
[_errorMessages:protected] => Array
(
)
[_errors:protected] => Array
(
)
[_isErrorForced:protected] =>
[_label:protected] => Current City
[_value:protected] => 93399
[class] => field_container field_19 option_1 parent_1
)
I want to get value property of the object. When I try $obj->_value or $obj->value it generates error. I searched and found the solution to use PHP Reflection Class. It worked on my local but on server PHP version is 5.2.17 So I cannot use this function there. So any solution how to get such property?
Here's the really simple example (with no error checking) of how to use ReflectionClass:
function accessProtected($obj, $prop) {
$reflection = new ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
return $property->getValue($obj);
}
I know you said you were limited to 5.2, but that was 2 years ago, 5.5 is the oldest supported version and I'm hoping to help people with modern versions.
Object can be typecasted into (associative) array and the protected members have keys prefixed with chr(0).'*'.chr(0) (see #fardelian's comment here). Using this undocummented feature you can write an "exposer":
function getProtectedValue($obj, $name) {
$array = (array)$obj;
$prefix = chr(0).'*'.chr(0);
return $array[$prefix.$name];
}
Alternatively, you can parse the value from serialized string, where (it seems) protected members have the same prefix.
This works in PHP 5.2 without the overhead of ReflectionClass. However, there are reasons why some property is protected and hidden from client code. The reading or writing can make the data inconsistent or the author provides some other way to expose it in effort to make the interface as lean as possible. When there are reasons to read the protected property directly, the then-correct approach was to implement __get() magic method, so always check if there is any and see what it does. This counter intuitive lookup was finally solved in PHP 8.1 with readonly properties.
Since PHP 8.0, there also attributes metadata accessible by ReflectionClass, make sure to check them also before performing attempts to break into protected members. Attributes superseded "Annotations"1, so check them, too.
1: annotations are a very nasty surprise to client coders: they parse comments to add crazy fancy black-box useless confusing functionality, should not be used anymore, but they still exist
That's what "protected" is meant for, as the Visibility chapter explains:
Members declared protected can be accessed only within the class itself and by inherited and parent classes.
If you need to access the property from outside, pick one:
Don't declare it as protected, make it public instead
Write a couple of functions to get and set the value (getters and setters)
If you don't want to modify the original class (because it's a third-party library you don't want to mess) create a custom class that extends the original one:
class MyFields_Form_Element_Location extends Fields_Form_Element_Location{
}
... and add your getter/setter there.
If you want to tinker with a class without adding getters and setters....
PHP 7 adds a call($obj) method (faster than old bindTo) on closures allowing you to call a function so the $this variable will act just as it would within a class -with full permissions.
//test class with restricted properties
class test{
protected $bar="protected bar";
private $foo="private foo";
public function printProperties(){
echo $this->bar."::".$this->foo;
}
}
$testInstance=new test();
//we can change or read the restricted properties by doing this...
$change=function(){
$this->bar="I changed bar";
$this->foo="I changed foo";
};
$change->call($testInstance);
$testInstance->printProperties();
//outputs I changed bar::I changed foo in php 7.0
For PHP 7.4+, we can use an Arrow Function and the Closure::call to access private and protected members using just one small line:
PHP 7.4+
Retrieving protected/private members:
class Test {
protected $data = 'Protected variable!';
}
// Will output "Protected variable!"
echo (fn() => $this->data)->call(new Test);
Altering protected/private members:
class Test {
protected $data = 'Testing';
}
$test = new Test;
(fn() => $this->data = "New Data!")->call($test);
// Will output "New Data!"
echo (fn() => $this->data)->call($test);
Of course, we can use a normal Closure function if we want to alter/use multiple members:
class Test {
protected $data = 'Data!';
}
$test = new Test;
(function() {
$this->new_data = "New {$this->data}";
})->call($test);
// Will output "New Data!"
echo (fn() => $this->new_data)->call($test);
If you cannot modify the original class and extending it is not an option either, you can use the ReflectionProperty interface.
The phptoolcase library has a handy method for this:
$value = PtcHandyMan::getProperty($your_object , 'propertyName');
Static property from a singleton class:
$value = PtcHandyMan::getProperty('myCLassName', 'propertyName');
You can find the tool here: http://phptoolcase.com/guides/ptc-hm-guide.html
$a=json_encode((array)$obj);
$b=(array)json_decode(str_replace('\u0000*\u0000','',$a));
echo($b['value']);
What I like to do is declare every property that would be writable from outside as public. The properties that you want to be visible for the outside world but not writable you should declare as protected and write __get() magic method so you can read them. Example:
/**
* Class Test
*
* #property int $protected
*
*/
class Test
{
private const READABLE = ['protected'];
protected $protected = 1;
public $public = 2;
public function __get($property)
{
//if you want to read every protected or private
return $this->$property ?? null;
//if you want only some protected and private values to be readable
if (in_array($property, self::READABLE)) {
return $this->$property;
}
}
}
$test = new Test();
echo $test->protected; //outputs 1
echo $test->public; //outputs 2
$test->protected = 3; //outputs error - protected property
The best would be to have property declaration like:
public readonly $protected = 1; //only readable from the outside
public $public = 2; //readable and writable from the outside
but no such syntax exists yet(or... at least I don't know about it). P.S. you should declare the protected/private properties that will be readable in the Class DockBlock as shown, so you can autocomplete them, otherwise you will be able to access them, but your IDE won't recognize them on autocomplete when you are writing code.
I have a object having some protected property that I want to get and set. The object looks like
Fields_Form_Element_Location Object
(
[helper] => formText
[_allowEmpty:protected] => 1
[_autoInsertNotEmptyValidator:protected] => 1
[_belongsTo:protected] =>
[_description:protected] =>
[_disableLoadDefaultDecorators:protected] =>
[_errorMessages:protected] => Array
(
)
[_errors:protected] => Array
(
)
[_isErrorForced:protected] =>
[_label:protected] => Current City
[_value:protected] => 93399
[class] => field_container field_19 option_1 parent_1
)
I want to get value property of the object. When I try $obj->_value or $obj->value it generates error. I searched and found the solution to use PHP Reflection Class. It worked on my local but on server PHP version is 5.2.17 So I cannot use this function there. So any solution how to get such property?
Here's the really simple example (with no error checking) of how to use ReflectionClass:
function accessProtected($obj, $prop) {
$reflection = new ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
return $property->getValue($obj);
}
I know you said you were limited to 5.2, but that was 2 years ago, 5.5 is the oldest supported version and I'm hoping to help people with modern versions.
Object can be typecasted into (associative) array and the protected members have keys prefixed with chr(0).'*'.chr(0) (see #fardelian's comment here). Using this undocummented feature you can write an "exposer":
function getProtectedValue($obj, $name) {
$array = (array)$obj;
$prefix = chr(0).'*'.chr(0);
return $array[$prefix.$name];
}
Alternatively, you can parse the value from serialized string, where (it seems) protected members have the same prefix.
This works in PHP 5.2 without the overhead of ReflectionClass. However, there are reasons why some property is protected and hidden from client code. The reading or writing can make the data inconsistent or the author provides some other way to expose it in effort to make the interface as lean as possible. When there are reasons to read the protected property directly, the then-correct approach was to implement __get() magic method, so always check if there is any and see what it does. This counter intuitive lookup was finally solved in PHP 8.1 with readonly properties.
Since PHP 8.0, there also attributes metadata accessible by ReflectionClass, make sure to check them also before performing attempts to break into protected members. Attributes superseded "Annotations"1, so check them, too.
1: annotations are a very nasty surprise to client coders: they parse comments to add crazy fancy black-box useless confusing functionality, should not be used anymore, but they still exist
That's what "protected" is meant for, as the Visibility chapter explains:
Members declared protected can be accessed only within the class itself and by inherited and parent classes.
If you need to access the property from outside, pick one:
Don't declare it as protected, make it public instead
Write a couple of functions to get and set the value (getters and setters)
If you don't want to modify the original class (because it's a third-party library you don't want to mess) create a custom class that extends the original one:
class MyFields_Form_Element_Location extends Fields_Form_Element_Location{
}
... and add your getter/setter there.
If you want to tinker with a class without adding getters and setters....
PHP 7 adds a call($obj) method (faster than old bindTo) on closures allowing you to call a function so the $this variable will act just as it would within a class -with full permissions.
//test class with restricted properties
class test{
protected $bar="protected bar";
private $foo="private foo";
public function printProperties(){
echo $this->bar."::".$this->foo;
}
}
$testInstance=new test();
//we can change or read the restricted properties by doing this...
$change=function(){
$this->bar="I changed bar";
$this->foo="I changed foo";
};
$change->call($testInstance);
$testInstance->printProperties();
//outputs I changed bar::I changed foo in php 7.0
For PHP 7.4+, we can use an Arrow Function and the Closure::call to access private and protected members using just one small line:
PHP 7.4+
Retrieving protected/private members:
class Test {
protected $data = 'Protected variable!';
}
// Will output "Protected variable!"
echo (fn() => $this->data)->call(new Test);
Altering protected/private members:
class Test {
protected $data = 'Testing';
}
$test = new Test;
(fn() => $this->data = "New Data!")->call($test);
// Will output "New Data!"
echo (fn() => $this->data)->call($test);
Of course, we can use a normal Closure function if we want to alter/use multiple members:
class Test {
protected $data = 'Data!';
}
$test = new Test;
(function() {
$this->new_data = "New {$this->data}";
})->call($test);
// Will output "New Data!"
echo (fn() => $this->new_data)->call($test);
If you cannot modify the original class and extending it is not an option either, you can use the ReflectionProperty interface.
The phptoolcase library has a handy method for this:
$value = PtcHandyMan::getProperty($your_object , 'propertyName');
Static property from a singleton class:
$value = PtcHandyMan::getProperty('myCLassName', 'propertyName');
You can find the tool here: http://phptoolcase.com/guides/ptc-hm-guide.html
$a=json_encode((array)$obj);
$b=(array)json_decode(str_replace('\u0000*\u0000','',$a));
echo($b['value']);
What I like to do is declare every property that would be writable from outside as public. The properties that you want to be visible for the outside world but not writable you should declare as protected and write __get() magic method so you can read them. Example:
/**
* Class Test
*
* #property int $protected
*
*/
class Test
{
private const READABLE = ['protected'];
protected $protected = 1;
public $public = 2;
public function __get($property)
{
//if you want to read every protected or private
return $this->$property ?? null;
//if you want only some protected and private values to be readable
if (in_array($property, self::READABLE)) {
return $this->$property;
}
}
}
$test = new Test();
echo $test->protected; //outputs 1
echo $test->public; //outputs 2
$test->protected = 3; //outputs error - protected property
The best would be to have property declaration like:
public readonly $protected = 1; //only readable from the outside
public $public = 2; //readable and writable from the outside
but no such syntax exists yet(or... at least I don't know about it). P.S. you should declare the protected/private properties that will be readable in the Class DockBlock as shown, so you can autocomplete them, otherwise you will be able to access them, but your IDE won't recognize them on autocomplete when you are writing code.
When you implement the _toString method on a class, you are able to convert the object in string
$string =(string) $object
Is there an equivalent for converting in array
$array=(array) $object
From what I have tested, with this code, the attributes of the objet are transformed in index of the array, even if this object implement ArrayAccess.
I expected that casting an object with array access, I would obtain an array thith the same values I could access with the object
public class MyObject implements ArrayAccess{
private $values;
public function __construct(array $values){
$this->values=$values;
}
public function offsetSet($name,$value){
$this->values[$name]=$value;
}
//etc...
}
$myObject=new MyObject(array('foo'=>'bar');
$asArray=(array)$myObject;
print_r($asArray);
// expect array('foo'=>'bar')
// but get array('MyObjectvalues'=>array('foo'=>'bar'));
I also Notice that the native ArrayObject class has a the behavior I expected
No, there is no magic function to cast object as array.
ArrayObject is implemented with C and has weird specific behaviors.
Implement custom method asArray and use it.
Actually, it's impossible to write a general function:
/*
* #return array ArrayAccess object converted into an array
*/
function (ArrayAccess $arrayAccessObject): array { /* ... */ }
Why? Because ArrayAccess interface just gives a way to use $aa[/*argument*/] syntax, but does not give a way to iterate over all possible arguments.
We used to think that array has a finite number of keys. However ArrayAccess let us create objects having an infinite set of keys (note, the same concerns Traversable: i.e. prime numbers are "traversable").
For example, one can write a class, implementing ArrayAccess, that acts like a HTTP client with a cache (I'm not saying that it's a good idea; it's just an example). Then offsetExists($url) tells if a URL gives 200 or not, offsetGet($url) returns a content of a URL, offsetUnset($url) clears cached content, offsetSet throws a LogicException, 'cause setting a value makes no sense in this context.
// ...
if (empty($client['https://example.com/file.csv'])) {
throw new RuntimeException('Cannot download the file');
}
$content = $client['https://example.com/file.csv'];
// ...
Or maybe one wants to read/write/unset (delete) files with ArrayAccess.
Or maybe something like (set of even numbers is infinite):
$even = new EvenNumberChecker(); // EvenNumberChecker implements ArrayAccess
$even[2]; // true
$even[3]; // false
$even[5.6]; // throws UnexpectedValueException
isset($even[7.8]); // false
$even[0] = $value; // throws LogicException
ArrayAccess objects from academic examples above cannot be converted into finite arrays.
You can use json_decode and json_encode to get the most generic function for it:
public static function toArray(ArrayAccess $array): array
{
return json_decode(
json_encode($array),
true
);
}
The I18n class in CakePHP provides this method to create instances:
public static function getInstance() {
static $instance = array();
if (!$instance) {
$instance[0] = new I18n();
}
return $instance[0];
}
Among other considerations (please correct me if I'm wrong), I understand it helps to use class instances from the convenience functions:
/**
* Returns a translated string if one is found; Otherwise, the submitted message.
*/
function __($singular, $args = null) {
// ...
$translated = I18n::translate($singular);
// ...
}
echo __('Hello, World!');
This looks cleaner than having to pass the instance around as argument (or, even worse, using a randomly named global variable). But I can't imagine a reason why $instance is an array rather than a plain object.
What can be the purpose of using a one-item array to store class instances?
I would suspect this to be leftovers from older PHP4/CakePHP versions where the instances were assigned by reference.
https://github.com/cakephp/cakephp/blob/1.2.0/cake/libs/i18n.php
function &getInstance() {
static $instance = array();
if (!$instance) {
$instance[0] =& new I18n();
$instance[0]->l10n =& new L10n();
}
return $instance[0];
}
$_this =& I18n::getInstance();
Assigning by reference doesn't work with static, the reference is not being remembered, but it works when assigned to an array entry.
So this was most probably just a workaround for a PHP limitation.
One possible reason for this is to keep all singleton class instances in one global - (static is a synonym of global in this case) array variable for monitoring or not messing the global/local namespace with individual variables for each singleton. If each of the static variables were with random names e.g $translated it would be more easier to overwrite and mess its value. - bug again for me, this is extremely rear possibility.
For example the I18Nn instance would be with [0] key, other class would have other key. You should check outher singleton classes how manage the static $instance array values.
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.
__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.
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.
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.
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.
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.
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
Drop the public $bar; declaration and it should work as expected.
Intenta con:
__GET($k){
return $this->$k;
}
_SET($k,$v){
return $this->$k = $v;
}