- URL: https://www.laruence.com/en/2011/09/22/2152.html
- Please include attribution when republishing.
Today on bugs.php.net, a user with a QQ mailbox filed a bug (#55731).
He asked: why does the following code call the getter twice?
<?php
class Example{
private $p1;
private $p2;
function __construct($a){
$this->p1=$a;
}
function __get($elmname){
echo "Call_get()";
return $this->$elmname;
}
function __isset($name){
return isset($this->$name);
}
function __unset($name){
unset($this->$name);
}
}
$example = new Example("v1");
unset($example->p1);
echo $example->p1;
//output
//Call_get()Call_get()
At first I just gave a brief answer, saying it was related to him fetching $this->elmname again inside __get. Later this person pressed me on the reason, so I had to explain it in my poor English.
I probably didn't explain it clearly in English, so let me explain it in Chinese now.
(Supplement: after the article was published, several people felt I had over-complicated a simple problem — they thought unset($example->p1) would also trigger a getter call. So let me address that first: contexts that are not "read" contexts do not trigger a call to __get. This is also easy to verify, everyone can try it. Otherwise I wouldn't have written this article to discuss the point.)
First. The key reason for this problem is: unsetting a private variable.
When we read an object's property (a "read" context), it is actually first compiled into the ZEND_FETCH_OBJ_R intermediate instruction (opcode). Then at runtime, this opcode eventually leads to a call to zend_read_property. I list the key code below:
.....
/* make zend_get_property_info silent if we have getter - we may want to use it */
property_info = zend_get_property_info_quick(zobj->ce, member, (zobj->ce->__get != NULL), key TSRMLS_CC);
if (UNEXPECTED(!property_info) ||
((EXPECTED((property_info->flags & ZEND_ACC_STATIC) == 0) &&
property_info->offset >= 0) ?
(zobj->properties ?
((retval = (zval**)zobj->properties_table[property_info->offset]) == NULL) :
(*(retval = &zobj->properties_table[property_info->offset]) == NULL)) :
(UNEXPECTED(!zobj->properties) ||
UNEXPECTED(zend_hash_quick_find(zobj->properties, property_info->name,
property_info->name_length+1, property_info->h, (void **) &retval) == FAILURE)))) {
zend_guard *guard = NULL;
if (zobj->ce->__get &&
zend_get_property_guard(zobj, property_info, member, &guard) == SUCCESS &&
!guard->in_get) {
/* have getter - try with it! */
Z_ADDREF_P(object);
if (PZVAL_IS_REF(object)) {
SEPARATE_ZVAL(&object);
}
guard->in_get = 1; /* prevent circular getting */
rv = zend_std_call_getter(object, member TSRMLS_CC);
guard->in_get = 0;
The above code is explained as follows:
1. First it calls zend_get_property_info_quick, trying to find the declaration info for the property (public, protect, name, hash) in the object's class (zend_class_entry * zobj->ce). zend_get_property_info_quick returns NULL when it can't find the property, or when it finds it but access is not allowed (external access to a private or protected property).
2. If the corresponding property info is found, then it uses the property name in the property info as the name to look up next (in PHP, a private property is named "\0class_name\0property_name\0", a protected property is named: "\0*\0property_name\0", and a public property is named "property_name\0").
2. If the relevant declaration info is not found (an undefined property), then it tries to look directly in the object's property set (zobj_properties, because PHP is a very flexible language and you can dynamically add properties to an object). If it finds one, it returns successfully.
3. If it's not found in the object's property set either, then it checks whether the object declares a __get magic method. If not, it reports "not found" and returns failure.
4. If there is a __get magic method, in order to avoid nested recursion, it first checks whether a guard for that property name already exists. If so, it checks whether guard->in_get is true; if true, that means recursion has occurred, so it returns failure. If not, it sets a guard named after the property (note this point), then calls __get.
5. If calling __get finds it, it returns successfully; otherwise it ends in failure.
Now, let's look at the example at the beginning of the article.
1. Call zend_read_property, zobj is $example, member is p1.
2. Call zend_get_property_info_quick to look up the p1 property info. Because the current scope is the global scope, PHP does not allow direct access to an object's private property, so zend_get_property_info_quick returns NULL.
3. Try to look for p1 in zobj->properties. Because p1 has been unset, it doesn't exist — not found.
4. Discover that $example has a __get magic method.
5. Check whether a guard has been set for "p1". No.
6. Set a guard named "p1", then call $example->__get (outputs Call_get()).
7. Inside $example->__get, we try to fetch $this->p1, so we go through it again::
8. Call zend_read_property, zobj is $example, member is p1.
9. Call zend_get_property_info_quick to look up the p1 property info. Because the current scope is example, zend_get_property_info_quick returns successfully.
10. Use the property name in the returned property info "\0example\0p1\0" as the name to look up.
11. Try to look for p1 in zobj->properties. Because p1 has been unset, it doesn't exist — not found.
12. Discover that $example has a __get magic method.
13. Check whether a guard has been set for "\0example\0p1\0". No.
14. Set a guard named "\0example\0p1\0", then call $example->__get (outputs Call_get()).
15. Inside $example->__get, we try to fetch $this->p1, so we go through it again::
Then repeat 8, 9, 10, 11, 12.
16. Check whether a guard has been set for "\0example\0p1\0" — there is, recursion has occurred, report an error, and return failure.
Be First to Comment