Press "Enter" to skip to content

PHP Internals — Objects (Part 1)

Before PHP4, PHP had no support for object orientation. With PHP4, PHP introduced a set of OOP keywords — and note that I say "keywords", because an object in PHP4 was nothing more than an array (the properties) plus an array of functions (the methods): no access control, no destructor (you could emulate one, of course), and so on.
Then came PHP5, and with the release of Zend Engine 2:

1. Access control
2. Interfaces
3. Magic methods (PHP4 could emulate these to a limited extent through overloading)
4. Applying interfaces
5. Built-in interfaces
and so on.

PHP5 could finally be called a fairly complete object-oriented implementation.
Yet all these seemingly complex features have never fundamentally departed from the property array + method array basics. Next I will uncover the secrets hidden in the source code.
The Structure of an Object
In PHP5 an object is still carried by a zval. Do you still remember what a zval is (Deep dive into PHP internals: variables).

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

If a zval holds an object, then the obj member of zvalue_value points to a zend_object_value instance.
A zend_object_value has two members: one is an identifier (an integer handle) telling where the object currently sits in the global object list, and the other is a zend_object_handlers pointer, pointing to the handlers (the set of standard operations) of the class the object belongs to.
The real object entity, zend_object, holds the following key entry points:

1. ce, zend_class_entry the class entry
2. properties, hashTable the set of ordinary properties

Object Properties
As described above, ordinary properties live in a HashTable. PHP5 introduced access control, and a property's access level is distinguished by its name (to this end Zend introduced zend_mangle_property_name).

1. public  property name
2. private class nameproperty name
3. protected *property name

PHP marks property access levels with this rather ugly but simple and efficient trick. Knowing it, we can do some quite unreasonable things, such as reaching an object's private/protected properties (see: Bug #44273 access to private and protected class variables allowed when casting to array):

class Foo {
    private $_name = "laruence";
    protected $_age = 28;
}
$foo = new Foo();
$arr = (array) $foo;
var_dump($arr["Foo_name"]);
var_dump($arr["*_age"]);
//output:
string(8) "laruence"
int(28)

Since I've mentioned ordinary properties, there are also not-so-ordinary ones: Static and Constant. Because static properties and constants are tied to the class rather than to the object, it follows naturally that they should be stored in the class structure, that is, in zend_class_entry. An object's methods are likewise bound to the class.
zend_class_entry has the following HashTable members:

1. function_table, the methods
2. default_static_members, static properties
3. default_properties, the default properties
4. constants_table, the constants table

True to their name, ordinary properties are stored among the object's members.
Object Methods
The function_table inside zend_class_entry is a HashTable, and this function table has the same structure as an ordinary function table (Deep dive into PHP internals: functions); it likewise ends up carried by a zend_op_array. So, just like ordinary functions, method names are case-insensitive and can carry arg_info. The difference is that a function's access level is indicated by the fn_flag member of zend_op_array.
As we know, functions inside PHP all share a uniform parameter list (as long as you follow the PHP function conventions when writing them),

#define INTERNAL_FUNCTION_PARAM_PASSTHRU ht, return_value, return_value_ptr, this_ptr, return_value_used TSRMLS_CC

When a method is called, the this keyword is indicated by this_ptr among the function parameters, while for user-defined functions the this keyword is guaranteed by the Zend VM.
Speaking of which, here's an example:
The following code produces a Fatal error:

<?php
class Foo {
   public function Say() {
      $this = NULL;
   }
}
?>
//output:
PHP Fatal error:  Cannot re-assign $this in **

This is a rudimentary protection measure preventing the this keyword from being rewritten. But is that all PHP does to protect it? Not at all — let's bypass that protection and see, as follows:

<?php
class Foo {
    public $id  = "laruence";
    function Say($arr) {
        extract($arr, EXTR_OVERWRITE);
        var_dump($this);
        var_dump($this->id);
    }
}
$a = new Foo();
$a->sAY(array("this" => NULL)); //just to show that method names are case-insensitive.
?>
//output:
NULL
string(8) "laruence"

So the this keyword is not simply an item in the symbol table. Its correctness is guaranteed by the Zend Engine already at the parsing stage, and it is attached to the function's structure.
Standard Object Operations
The operations we perform on objects — reading a property, writing a property, getting an object's class, and so on — are all implemented at the object's standard methods. PHP5 provides 23 standard methods.
The handlers pointer in zend_object_value points to the set of methods for a class's common operations. By default, this pointer points to:

ZEND_API zend_object_handlers std_object_handlers = {
    zend_objects_store_add_ref,             /* add_ref */
    zend_objects_store_del_ref,             /* del_ref */
    zend_objects_clone_obj,                 /* clone_obj */
    zend_std_read_property,                 /* read_property */
    zend_std_write_property,                /* write_property */
    zend_std_read_dimension,                /* read_dimension */
    zend_std_write_dimension,               /* write_dimension */
    zend_std_get_property_ptr_ptr,          /* get_property_ptr_ptr */
    NULL,                                   /* get */
    NULL,                                   /* set */
    zend_std_has_property,                  /* has_property */
    zend_std_unset_property,                /* unset_property */
    zend_std_has_dimension,                 /* has_dimension */
    zend_std_unset_dimension,               /* unset_dimension */
    zend_std_get_properties,                /* get_properties */
    zend_std_get_method,                    /* get_method */
    NULL,                                   /* call_method */
    zend_std_get_constructor,               /* get_constructor */
    zend_std_object_get_class,              /* get_class_entry */
    zend_std_object_get_class_name,         /* get_class_name */
    zend_std_compare_objects,               /* compare_objects */
    zend_std_cast_object_tostring,          /* cast_object */
    NULL,                                   /* count_elements */
};

By default, and in the vast majority of cases, it is these standard methods shown above.
It should be pointed out that methods with the _dimension suffix are the ones used to access an object's properties the array way ($obj['name']).
Magic Methods
Magic methods are defined in class_entry,

union _zend_function *constructor;
union _zend_function *destructor;
union _zend_function *clone;
union _zend_function *__get;
union _zend_function *__set;
union _zend_function *__unset;
union _zend_function *__isset;
union _zend_function *__call;
union _zend_function *__tostring;
union _zend_function *serialize_func;
union _zend_function *unserialize_func;

Magic methods have no default value; they are assigned at the moment the class is defined.
For some magic methods, such as __get/__set, the standard handlers are what trigger the call. So if we write our own class in an extension and don't use the standard handlers, we must call these magic methods ourselves at the appropriate moment.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.