First, class introduction

Classes, interfaces, and traits in PHP are implemented as zend_class_entry structures at the bottom

struct _zend_class_entry {
	char type;
	const char *name;
	zend_uint name_length;
	struct _zend_class_entry *parent;
	int refcount;
	zend_uint ce_flags;

	HashTable function_table;
	HashTable properties_info;
	zval **default_properties_table;
	zval **default_static_members_table;
	zval **static_members_table;
	HashTable constants_table;
	int default_properties_count;
	int default_static_members_count;

	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* __callstatic;
	union _zend_function* __tostring;
	union _zend_function *serialize_func;
	union _zend_function *unserialize_func;

	zend_class_iterator_funcs iterator_funcs;

	/* handlers */
	zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC);
	zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC);
	int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* a class implements this interface */
	union _zend_function* (*get_static_method) (zend_class_entry *ce.char* method.int method_len TSRMLS_DC);

	/* serializer callbacks */
	int (*serialize)(zval *object, unsigned char **buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC);
	int (*unserialize)(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC);

	zend_class_entry **interfaces;
	zend_uint num_interfaces;
	
	zend_class_entry **traits;
	zend_uint num_traits;
	zend_trait_alias **trait_aliases;
	zend_trait_precedence **trait_precedences;

	union {
		struct {
			const char *filename;
			zend_uint line_start;
			zend_uint line_end;
			const char *doc_comment;
			zend_uint doc_comment_len;
		} user;
		struct {
			const struct _zend_function_entry *builtin_functions;
			struct _zend_module_entry *module;
		} internal;
	} info;
};
Copy the code

The Zend_class_entry structure contains a large number of Pointers and hashtables, which causes the structure itself to take up a lot of memory space. In addition, Pointers in the structure need to be allocated separately, which consumes some memory space.

1. Comparison between developer – defined classes and PHP – defined classes

Developer – defined classes are defined in the PHP language, and PHP – defined classes are defined in the PHP source code or in PHP extensions. The essential difference is the life cycle:

  • In the case of PHP-Fpm, when a request arrives, PHP parses the developer-defined class and allocates the appropriate memory space for it. PHP then makes calls to these classes as the request is processed, and finally destroys them after the request is processed, freeing up the memory allocated for them.

To save memory, don’t define classes in your code that you don’t actually use. You can use autoload to mask classes that are not actually used, because autoload only loads and parses a class when it is used, but this will delay the parsing and loading of the class from the compilation phase of the code to the execution phase of the code. It is also important to note that even with the OPCache extension enabled, the dev-defined classes are still parsed and loaded upon request, and destroyed upon completion. OPCache only speeds up these two phases

  • Classes defined internally in PHP are different. Again using phP-fpm as an example, when a phP-fpm process is started, PHP permanently allocates memory space for these classes until the process dies (to avoid memory leaks, PHP-fpm is destroyed and restarted after a certain number of requests have been processed).
if (EG(full_tables_cleanup)) {
	zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function_full TSRMLS_CC);
	zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class_full TSRMLS_CC);
} else {
	zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function TSRMLS_CC);
	zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class TSRMLS_CC);
}

static int clean_non_persistent_class(zend_class_entry **ce TSRMLS_DC)
{
	return ((*ce)->type == ZEND_INTERNAL_CLASS) ? ZEND_HASH_APPLY_STOP : ZEND_HASH_APPLY_REMOVE;
}
Copy the code

As you can see from the above code, classes defined internally in PHP are not destroyed at the end of the request. In addition, because classes defined in PHP extensions fall under the category of classes defined within PHP, you should not open extensions that you do not use to save memory. Because, if the extension is started, the classes defined in the extension are parsed and loaded when the PHP-fpm process starts.

Many times, for convenience, we will define exceptions by inherits \Exception. However, because the Zend_class_entry structure is so large, this results in a lot of memory consumption while increasing convenience

This class binding

Class binding refers to the process of preparing class data

For classes defined internally in PHP, the binding process is completed at class registration. This process occurs before the PHP script runs and only once throughout the life of the PHP-fpm process.

For classes that do not inherit the parent class, implement interfaces, or use traits, the binding process takes place during the editing phase of the PHP code and does not consume much resources. Binding such a class usually requires only registering the class with class_table and checking to see if the class contains abstract methods but is not declared as abstract types.

void zend_do_early_binding(TSRMLS_D) {{{/ * * /
{
	zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last- 1];
	HashTable *table;

	while (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)->opcodes) {
		opline--;
	}

	switch (opline->opcode) {
		case ZEND_DECLARE_FUNCTION:
			if (do_bind_function(CG(active_op_array), opline, CG(function_table), 1) == FAILURE) {
				return;
			}
			table = CG(function_table);
			break;
		case ZEND_DECLARE_CLASS:
			if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1 TSRMLS_CC) == NULL) {
				return;
			}
			table = CG(class_table);
			break;
		case ZEND_DECLARE_INHERITED_CLASS:
			{
				/ *... . * /
			}
		case ZEND_VERIFY_ABSTRACT_CLASS:
		case ZEND_ADD_INTERFACE:
		case ZEND_ADD_TRAIT:
		case ZEND_BIND_TRAITS:
			/* We currently don't early-bind classes that implement interfaces */
			/* Classes with traits are handled exactly the same, no early-bind here */
			return;
		default:
			zend_error(E_COMPILE_ERROR, "Invalid binding type");
			return;
	}

/ *... . * /
}

void zend_verify_abstract_class(zend_class_entry *ce TSRMLS_DC)
{
	zend_abstract_info ai;

	if((ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) && ! (ce->ce_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) {memset(&ai, 0.sizeof(ai));

		zend_hash_apply_with_argument(&ce->function_table, (apply_func_arg_t) zend_verify_abstract_class_function, &ai TSRMLS_CC);

		if (ai.cnt) {
			zend_error(E_ERROR, "Class %s contains %d abstract method%s and must therefore be declared abstract or implement the remaining methods (" MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT ")",
				ce->name, ai.cnt,
				ai.cnt > 1 ? "s" : "",
				DISPLAY_ABSTRACT_FN(0),
				DISPLAY_ABSTRACT_FN(1),
				DISPLAY_ABSTRACT_FN(2)); }}}Copy the code

The binding process for classes that implement interfaces is very complicated. The general process is as follows:

  • Check whether the interface is implemented
  • Check that the interface is actually implemented by a class, not by the interface itself (the underlying data structure for classes, interfaces, and traits is zend_class_entry).
  • Copy the constants and check for possible conflicts
  • In addition to copying methods and checking for possible conflicts, you need to check access control
  • Add interface to zend_class_entry**interfaces

Note that copying simply increases the reference count of constants, properties, and methods by one

ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface TSRMLS_DC)
{
	/ *... . * /
	
	} else {
		if (ce->num_interfaces >= current_iface_num) { /* resize the vector if needed */
			if (ce->type == ZEND_INTERNAL_CLASS) {
				/* For internally defined classes, use RealLoc to allocate memory that is permanently valid for the lifetime of the process */
				ce->interfaces = (zend_class_entry **) realloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num));
			} else {
				/* For developer-defined classes, use erealloc to allocate memory for the lifetime of the request */
				ce->interfaces = (zend_class_entry **) erealloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num));
			}
		}
		ce->interfaces[ce->num_interfaces++] = iface; /* Add the interface to the class */

		/* Copy every constants from the interface constants table to the current class constants table */
		zend_hash_merge_ex(&ce->constants_table, &iface->constants_table, (copy_ctor_func_t) zval_add_ref, sizeof(zval *), (merge_checker_func_t) do_inherit_constant_check, iface);
		/* Copy every methods from the interface methods table to the current class methods table */
		zend_hash_merge_ex(&ce->function_table, &iface->function_table, (copy_ctor_func_t) do_inherit_method, sizeof(zend_function), (merge_checker_func_t) do_inherit_method_check, ce); do_implement_interface(ce, iface TSRMLS_CC); zend_do_inherit_interfaces(ce, iface TSRMLS_CC); }}Copy the code

For constant replication, zval_add_ref is used to increase the reference count of the constant by 1; For method copying, do_inherit_method increases the reference count of the corresponding method by 1, and also increases the reference count of the static variables defined in the method by 1.

static void do_inherit_method(zend_function *function)
{
	function_add_ref(function);
}

ZEND_API void function_add_ref(zend_function *function)
{
	if (function->type == ZEND_USER_FUNCTION) {
		zend_op_array *op_array = &function->op_array;

		(*op_array->refcount)++;
		if (op_array->static_variables) {
			HashTable *static_variables = op_array->static_variables;
			zval *tmp_zval;

			ALLOC_HASHTABLE(op_array->static_variables);
			zend_hash_init(op_array->static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0);
			zend_hash_copy(op_array->static_variables, static_variables, (copy_ctor_func_t) zval_add_ref, (void *) &tmp_zval, sizeof(zval *));
		}
		op_array->run_time_cache = NULL; }}Copy the code

Bindings for classes that implement interfaces are often CPU intensive because of the many iterations and checks that are required, but save memory.

At this stage, PHP is deferring the binding of interfaces to the code execution phase, assuming that this will happen on every request

For class-inherited bindings, the process is similar but more complex than for interface bindings. It is also worth noting that if the parent class has been resolved at binding time, the binding takes place during code compilation. Otherwise it happens during the code execution phase.

// A declares before B that B binding occurs at compile time
class A {}class B extends A {}// A declares after B that the compiler cannot know about A when B is bound, and that B's binding can only be deferred until code execution
class B extends A {}class A {}// Error: Class B doesn't exist
// C is bound to C during code execution, but B inherits from A, and A is still unknown
class C extends B {}class B extends A {}class A {}Copy the code

If you use Autoload and use a one class for one file mode, all class binding occurs only during code execution

Object in PHP 5

Manner object method

The underlying data structure for both methods and functions is zend_function. The PHP compiler compiles and adds methods to the function_table attribute of zend_class_entry at compile time. So, by the time the PHP code runs, the method has been compiled and all PHP has to do is find the method through the pointer and execute it.

typedef union _zend_function {
	zend_uchar type;

	struct {
		zend_uchar type;
		const char *function_name;
		zend_class_entry *scope;
		zend_uint fn_flags;
		union _zend_function *prototype;
		zend_uint num_args;
		zend_uint required_num_args;
		zend_arg_info *arg_info;
	} common;

	zend_op_array op_array;
	zend_internal_function internal_function;
} zend_function;
Copy the code

When an object tries to call a method, it first looks for the method in the function_table of its corresponding class, and it also checks the access control of the method. If the method does not exist or the method’s access control does not meet requirements, object attempts to call the exclusive method __call.

static inline union _zend_function *zend_get_user_call_function(zend_class_entry *ce, const char *method_name, int method_len) 
{
	zend_internal_function *call_user_call = emalloc(sizeof(zend_internal_function));
	call_user_call->type = ZEND_INTERNAL_FUNCTION;
	call_user_call->module = (ce->type == ZEND_INTERNAL_CLASS) ? ce->info.internal.module : NULL;
	call_user_call->handler = zend_std_call_user_call;
	call_user_call->arg_info = NULL;
	call_user_call->num_args = 0;
	call_user_call->scope = ce;
	call_user_call->fn_flags = ZEND_ACC_CALL_VIA_HANDLER;
	call_user_call->function_name = estrndup(method_name, method_len);

	return (union _zend_function *)call_user_call;
}

static union _zend_function *zend_std_get_method(zval **object_ptr, char *method_name, int method_len, const zend_literal *key TSRMLS_DC)
{
	zend_function *fbc;
	zval *object = *object_ptr;
	zend_object *zobj = Z_OBJ_P(object);
	ulong hash_value;
	char *lc_method_name;
	ALLOCA_FLAG(use_heap)

	if(EXPECTED(key ! =NULL)) {
		lc_method_name = Z_STRVAL(key->constant);
		hash_value = key->hash_value;
	} else {
		lc_method_name = do_alloca(method_len+1, use_heap);
		/* Create a zend_copy_str_tolower(dest, src, src_length); * /
		zend_str_tolower_copy(lc_method_name, method_name, method_len);
		hash_value = zend_hash_func(lc_method_name, method_len+1);
	}

	if (UNEXPECTED(zend_hash_quick_find(&zobj->ce->function_table, lc_method_name, method_len+1, hash_value, (void **)&fbc) == FAILURE)) {
		if(UNEXPECTED(! key)) { free_alloca(lc_method_name, use_heap); }if (zobj->ce->__call) {
			return zend_get_user_call_function(zobj->ce, method_name, method_len);
		} else {
			return NULL; }}/* Check access level */
	if (fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) {
		zend_function *updated_fbc;

		/* Ensure that if we're calling a private function, we're allowed to do so. * If we're not and __call() handler exists, invoke it, otherwise error out. */
		updated_fbc = zend_check_private_int(fbc, Z_OBJ_HANDLER_P(object, get_class_entry)(object TSRMLS_CC), lc_method_name, method_len, hash_value TSRMLS_CC);
		if(EXPECTED(updated_fbc ! =NULL)) {
			fbc = updated_fbc;
		} else {
			if (zobj->ce->__call) {
				fbc = zend_get_user_call_function(zobj->ce, method_name, method_len);
			} else {
				zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : ""); }}}else {
		/* Ensure that we haven't overridden a private function and end up calling * the overriding public function... * /
		if (EG(scope) &&
		    is_derived_class(fbc->common.scope, EG(scope)) &&
		    fbc->op_array.fn_flags & ZEND_ACC_CHANGED) {
			zend_function *priv_fbc;

			if (zend_hash_quick_find(&EG(scope)->function_table, lc_method_name, method_len+1, hash_value, (void**) &priv_fbc)==SUCCESS && priv_fbc->common.fn_flags & ZEND_ACC_PRIVATE && priv_fbc->common.scope == EG(scope)) { fbc = priv_fbc; }}if ((fbc->common.fn_flags & ZEND_ACC_PROTECTED)) {
			/* Ensure that if we're calling a protected function, we're allowed to do so. * If we're not and __call() handler exists, invoke it, otherwise error out. */
			if(UNEXPECTED(! zend_check_protected(zend_get_function_root_class(fbc), EG(scope)))) {if (zobj->ce->__call) {
					fbc = zend_get_user_call_function(zobj->ce, method_name, method_len);
				} else {
					zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : ""); }}}}if(UNEXPECTED(! key)) { free_alloca(lc_method_name, use_heap); }return fbc;
}
Copy the code

It should be pointed out here:

  • Since PHP is case-insensitive, all method names are changed to lowercase (zend_str_tolower_copy()).
  • To avoid unnecessary resource consumption, PHP 5.4 introduced the zend_literal structure, the key parameter
typedef struct _zend_literal {
	zval       constant;
	zend_ulong hash_value;
	zend_uint  cache_slot;
} zend_literal;
Copy the code

Where constant records the string converted to lowercase, hash_value is the precomputed hash. This prevents object from having to lowercase the method name and compute the hash value every time it calls a method.

class Foo { public function BAR() {}}$a = new Foo;
$b = 'bar';

$a->bar(); /* good */
$a->$b(a);/* bad */
Copy the code

In the above example, during code compilation, the method BAR is converted to BAR and added to the function_table of zend_class_entry. When a method call occurs:

  • In the first case, during the code compilation phase, the method name bar is determined to be a string constant, and the compiler can pre-calculate its corresponding Zend_literal structure, the key parameter. This way, the code executes relatively quickly.
  • In the second case, since the compiler doesn’t know anything about $b at compile time, it now converts the method name to lowercase and computs the hash value at code execution time.

⒉ Property in object

When a class is instantiated, the attributes in object are just references to the attributes in the class. In this way, the creation of an object is relatively lightweight and saves some memory.

If you want to modify a property in an object, the Zend engine creates a separate zval structure that affects only the current property of the current object.

The instantiation of a class corresponds to the creation of a Zend_OBEJCT data structure underneath, and the newly created object is registered in zend_objects_Store. Zend_objects_store is a global object registry in which an object can be registered only once.

typedef struct _zend_object {
	zend_class_entry *ce;
	HashTable *properties;
	zval **properties_table;
	HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;

typedef struct _zend_objects_store {/* Is essentially a dynamic array of object_buckets */
	zend_object_store_bucket *object_buckets;
	zend_uint top; /* The next available handle. Handle starts at 1. The index in *object_buckets is handle-1 */
	zend_uint size; /* Maximum length of the currently allocated *object_buckets */
	int free_list_head; /* When the bucket in the *object_bucket is destroyed, the index in the *object_bucket is added to the free_list in order. Free_list_head is the first value */ in the list
} zend_objects_store;

typedef struct _zend_object_store_bucket {
	zend_bool destructor_called;
	zend_bool valid; /* A value of 1 indicates that the current bucket is used. In this case, store_object in store_bucket is used. A value of 0 indicates that the current bucket does not store valid objects, in which case free_list in store_bucket is used */
	zend_uchar apply_count;
	union _store_bucket {
		struct _store_object {
			void *object;
			zend_objects_store_dtor_t dtor;
			zend_objects_free_object_storage_t free_storage;
			zend_objects_store_clone_t clone;
			const zend_object_handlers *handlers;
			zend_uint refcount;
			gc_root_buffer *buffered;
		} obj;
		struct {
			int next; /* The index of the first unused bucket is always stored in the free_list_head of the Zend_object_Store, so next only needs to record the index*/ of the first unused bucket after the current bucket
		} free_list;
	} bucket;
} zend_object_store_bucket;

ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC)
{
	zend_object_value retval;

	*object = emalloc(sizeof(zend_object));
	(*object)->ce = class_type;
	(*object)->properties = NULL;
	(*object)->properties_table = NULL;
	(*object)->guards = NULL;
	retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_storage, NULL TSRMLS_CC);
	retval.handlers = &std_object_handlers;
	return retval;
}
Copy the code

After registering an object in Zend_objects_store, properties are created for the object (references to the corresponding class properties)

ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type) 
{
	int i;

	if (class_type->default_properties_count) {
		object->properties_table = emalloc(sizeof(zval*) * class_type->default_properties_count);
		for (i = 0; i < class_type->default_properties_count; i++) {
			object->properties_table[i] = class_type->default_properties_table[i];
			if (class_type->default_properties_table[i]) {
#if ZTS
				ALLOC_ZVAL( object->properties_table[i]);
				MAKE_COPY_ZVAL(&class_type->default_properties_table[i], object->properties_table[i]);
#else
				Z_ADDREF_P(object->properties_table[i]);
#endif
			}
		}
		object->properties = NULL; }}Copy the code

It should be noted that when creating a property, if PHP is not in thread-safe mode, the reference count of the corresponding property is only increased; However, if PHP is in thread-safe mode, you need to make a deep copy of the properties, copying all the properties of the class to the properties_table in the object.

This also shows that thread-safe PHP runs slower and consumes more memory than non-thread-safe PHP

Each property corresponds to a Zend_property_info structure at the bottom level:

typedef struct _zend_property_info {
    zend_uint flags;
    const char *name;
    int name_length;
    ulong h;
    int offset;
    const char *doc_comment;
    int doc_comment_len;
    zend_class_entry *ce;
} zend_property_info;
Copy the code

For each property declared in class, there is a zend_property_info corresponding to it in the properties_table in zend_class_entry. Properties_table can help us quickly determine whether the properties accessed by an object exist:

  • If the property does not exist and we try to write the property to object: if class is defined__setMethod is used__setMethod writes the property; Otherwise, a dynamic property is added to object. However, no matter how the property is written, the written property is added to the object’s properties_table.
  • If the attribute exists, the corresponding access control needs to be checked; For protected and private types, you need to check the current scope.

After an object is created, as long as we do not write new attributes to the object or update the values of attributes in the corresponding class of the object, the memory occupied by the object will not change.

Zend_class_entry ->properties_info Stores each zend_property_info. The value of the property is actually stored as an array of ZVAL Pointers in zend_class_entry-> default_properties_TABLE. Properties added dynamically to an object are stored only in zend_object-> properties_TABLE in the form of property_NAME => property_value. When an object is created, the values in zend_class_entry->properties_table are passed to zend_object->properties_table one by one. The int stored in zend_literal->cache_slot is the index in run_time_cache. Run_time_cache is an array structure, and the value corresponding to index is the zend_class_entry corresponding to the object accessing this attribute. The value corresponding to index + 1 is the zend_property_info corresponding to the property. The zend_property_info structure can be quickly retrieved using zend_literal->cache_slot if the value in zend_literal->cache_slot is not empty when accessing the property; If empty, zend_literal->cache_slot is initialized after information about zend_property_info is retrieved.

Private property: “\0class_name\0property_name” protected property: “\0*\0property_name” public property: “property_name”

Execute the following code to see the output

class A {
    private $a = 'a';
    protected $b = 'b';
    public $c = 'c';
}

class B extends A {
    private $a = 'aa';
    protected $b = 'bb';
    public $c = 'cc';
}

class C extends B {
    private $a = 'aaa';
    protected $b = 'bbb';
    public $c = 'ccc';
}

var_dump(new C());
Copy the code

The role of Guards in Zend_object is to provide recursive protection against overloads of Object.

class Foo {
    public function __set($name.$value) {
        $this->$name = $value; }}$foo = new Foo;
$foo->bar = 'baz';
var_dump($foo->bar);
Copy the code

In the code above, the __set method is called when foo is set dynamically for foo. The $bar attribute does not exist in Foo, so the __set method is called recursively. To avoid this recursive call, PHP uses Zend_guard to determine if you are already in the context of an overloaded method.

typedef struct _zend_guard {
    zend_bool in_get;
    zend_bool in_set;
    zend_bool in_unset;
    zend_bool in_isset;
    zend_bool dummy; /* sizeof(zend_guard) must not be equal to sizeof(void*) */
} zend_guard;
Copy the code

A reference to the judge object is passed

First, declare that object is not passed by reference. The reason why object is passed by reference is because the parameter we pass to the function is the ID (handle) of the object in zend_objects_store. With this ID, we can find and load the real object in zend_objects_store, and then access and modify the properties in the object.

In PHP, there are two different scopes inside and outside a function. For the same variable, changing it inside the function does not affect the outside of the function. However, accessing and modifying the properties of an Object through the ID of the object (handle) is not subject to this restriction.

$a = 1;

function test($a) {
    $a = 3;
    echo $a; 3 / / output
}

test($a);

echo $a; / / output 1
Copy the code

The same object is stored only once in zend_objects_store. To write new objects to zend_objects_store, use only the new keyword, unserialize function, reflection, and Clone methods.

4. $this

$this automatically takes over the current object when used. PHP forbids assigning to this. Any assignment to this. Any assignment to this. Any assignment to this will cause an error

static zend_bool opline_is_fetch_this(const zend_op *opline TSRMLS_DC)
{
	if((opline->opcode == ZEND_FETCH_W) && (opline->op1_type == IS_CONST) && (Z_TYPE(CONSTANT(opline->op1.constant)) == IS_STRING) && ((opline->extended_value & ZEND_FETCH_STATIC_MEMBER) ! = ZEND_FETCH_STATIC_MEMBER) && (Z_HASH_P(&CONSTANT(opline->op1.constant)) == THIS_HASHVAL) && (Z_STRLEN(CONSTANT(opline->op1.constant)) == (sizeof("this")- 1)) &&!memcmp(Z_STRVAL(CONSTANT(opline->op1.constant)), "this".sizeof("this"))) {
	    return 1;
	} else {
	    return 0; }}/ *... . * /
if (opline_is_fetch_this(last_op TSRMLS_CC)) {
	zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");
}
/ *... . * /
Copy the code

When you make a method call in PHP, the corresponding OPCode is INIT_METHOD_CALL. Take $a->foo() as an example. In INIT_METHOD_CALL, the Zend engine knows that $a made the method call, so the Zend engine stores the value of $a in the global space. When the actual method call is executed, the corresponding OPCode is DO_FCALL. In DO_FCALL, the Zend engine assigns the value of $a previously stored in the global space to the pointer to $this, i.e. EG(this) :

if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) {
    should_change_scope = 1;
    EX(current_this) = EG(This);
    EX(current_scope) = EG(scope);
    EX(current_called_scope) = EG(called_scope);
    EG(This) = EX(object); /* fetch the object prepared in previous INIT_METHOD opcode and affect it to EG(This) */EG(scope) = (fbc->type == ZEND_USER_FUNCTION || ! EX(object)) ? fbc->common.scope :NULL;
    EG(called_scope) = EX(call)->called_scope;
}
Copy the code

$this->a = 8 = OPCode ZEND_ASSIGN_OBJ

static zend_always_inline zval **_get_obj_zval_ptr_ptr_unused(TSRMLS_D)
{
	if(EXPECTED(EG(This) ! =NULL)) {
		return &EG(This);
	} else {
		zend_error_noreturn(E_ERROR, "Using $this when not in object context");
		return NULL; }}Copy the code

When the Zend engine builds the method stack, $this is stored in the symbol table, just like any other variable. In this way, when $this is used for a method call or as an argument to a method, the Zend engine will get $this from the symbol table.

if(op_array->this_var ! =- 1 && EG(This)) {
    Z_ADDREF_P(EG(This)); /* For $this pointer */
    if(! EG(active_symbol_table)) { EX_CV(op_array->this_var) = (zval **) EX_CV_NUM(execute_data, op_array->last_var + op_array->this_var); *EX_CV(op_array->this_var) = EG(This); }else {
        if (zend_hash_add(EG(active_symbol_table), "this".sizeof("this"), &EG(This), sizeof(zval *), (void**) EX_CV_NUM(execute_data, op_array->this_var))==FAILURE) { Z_DELREF_P(EG(This)); }}}Copy the code

Finally, there is the scope issue. When a method call is made, the Zend engine sets the scope to EG(scope). EG(scope) is a Zend_class_entry type, that is, any operation on an object in a method is scoped to the class corresponding to the object. The same goes for checking access control for attributes:

ZEND_API int zend_check_protected(zend_class_entry *ce, zend_class_entry *scope) 
{
	zend_class_entry *fbc_scope = ce;

	/* Is the context that's calling the function, the same as one of * the function's parents? * /
	while (fbc_scope) {
		if (fbc_scope==scope) {
			return 1;
		}
		fbc_scope = fbc_scope->parent;
	}

	/* Is the function's scope the same as our current object context, * or any of the parents of our context? * /
	while (scope) {
		if (scope==ce) {
			return 1;
		}
		scope = scope->parent;
	}
	return 0;
}

static zend_always_inline int zend_verify_property_access(zend_property_info *property_info, zend_class_entry *ce TSRMLS_DC)
{
	switch (property_info->flags & ZEND_ACC_PPP_MASK) {
		case ZEND_ACC_PUBLIC:
			return 1;
		case ZEND_ACC_PROTECTED:
			return zend_check_protected(property_info->ce, EG(scope));
		case ZEND_ACC_PRIVATE:
			if ((ce==EG(scope) || property_info->ce == EG(scope)) && EG(scope)) {
				return 1;
			} else {
				return 0;
			}
			break;
	}
	return 0;
}
Copy the code

Because of these features, the following code works

class A
{
	private $a;

	public function foo(A $obj)
	{
		$this->a = 'foo';
		$obj->a  = 'bar'; /* yes, this is possible */}}$a = new A;
$b = new A;
$a->foo($b);
Copy the code

In PHP, the scope of object is the class corresponding to object

(a) Destructor approach destruct

In PHP, do not rely on the destruct method to destroy objects. This is because the destruct method is not called when a fatal PHP error occurs.

ZEND_API void zend_hash_reverse_apply(HashTable *ht, apply_func_t apply_func TSRMLS_DC)
{
	Bucket *p, *q;

	IS_CONSISTENT(ht);

	HASH_PROTECT_RECURSION(ht);
	p = ht->pListTail;
	while(p ! =NULL) {
		int result = apply_func(p->pData TSRMLS_CC);

		q = p;
		p = p->pListLast;
		if (result & ZEND_HASH_APPLY_REMOVE) {
			zend_hash_apply_deleter(ht, q);
		}
		if (result & ZEND_HASH_APPLY_STOP) {
			break;
		}
	}
	HASH_UNPROTECT_RECURSION(ht);
}

static int zval_call_destructor(zval **zv TSRMLS_DC) 
{
	if (Z_TYPE_PP(zv) == IS_OBJECT && Z_REFCOUNT_PP(zv) == 1) {
		return ZEND_HASH_APPLY_REMOVE;
	} else {
		returnZEND_HASH_APPLY_KEEP; }}void shutdown_destructors(TSRMLS_D) 
{
	zend_try {
		int symbols;
		do {
			symbols = zend_hash_num_elements(&EG(symbol_table));
			zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC);
		} while(symbols ! = zend_hash_num_elements(&EG(symbol_table))); zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC); } zend_catch {/* if we couldn't destruct cleanly, mark all objects as destructed anyway */
		zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
	} zend_end_try();
}
Copy the code

When called, the destruct method first traverses the symbol table backward, calling the destruct method on all objects whose reference count is 1. We then iterate through the global Object store, calling the destruct method for each object. If any errors occur during this process, the destruct method is stopped and all objects are marked as having been called.

class Foo { public function __destruct() { var_dump("destroyed Foo"); }}class Bar { public function __destruct() { var_dump("destroyed Bar"); }}/ / sample 1
$a = new Foo;
$b = new Bar;
"destroyed Bar"
"destroyed Foo"

/ / sample 2
$a = new Bar;
$b = new Foo;
"destroyed Foo"
"destroyed Bar"

/ / sample 3
$a = new Bar;
$b = new Foo;
$c = $b; $b = $b
"destroyed Bar"
"destroyed Foo"

/ / sample 4
class Foo { public function __destruct() { var_dump("destroyed Foo"); die(a); }}/* notice the die() here */
class Bar { public function __destruct() { var_dump("destroyed Bar"); }}$a = new Foo;
$a2 = $a;
$b = new Bar;
$b2 = $b;

"destroyed Foo"
Copy the code

Also, do not add any significant code to the destruct method

class Foo
{
	public function __destruct() { new Foo; } /* PHP will eventually crash */
}
Copy the code

Object destruction in PHP is divided into two stages: First call the destruct method (zend_object_store_bucket->bucket->obj->zend_objects_store_dtor_t), and then call the destruct method (destruct ->bucket->obj->zend_objects_store_dtor_t). Then free the memory (zend_object_store_bucket->bucket->obj-> Zend_objects_free_object_storage_t). The reason it’s executed in two phases is because user-level code is executed in Destruct, which is PHP code; The code that frees memory runs at the bottom of the system. Freeing memory will destroy the runtime environment of PHP. In order to make the PHP code in Destruct run normally, it is divided into two phases. In this way, the object is guaranteed not to be used during the memory freeing phase.

3. Object in PHP 7

Object in PHP 7 is basically unchanged at the user level compared to PHP 5; But on the underlying implementation, some optimizations have been made in terms of memory and performance.

The optimization of memory layout and management

(1) insert zend_object into zval; (2) insert zend_object into zval; This saves memory space and improves the efficiency of finding zend_object through zval

/*PHP 7 zend_object*/
struct _zend_object {
    zend_refcounted   gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

/*PHP 5 zend_object_value*/
typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;
Copy the code

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} Then, run the Handle to find zend_object_store_bucket in zend_object_Store and parse the object from the bucket. In PHP 7, zval stores the zend_object address pointer directly.

Secondly, properties_table uses the struct hack feature, so that zend_object and properties_table are stored in a continuous memory space. Also, the ZVAL structure of the property is directly stored in properties_table.

③ Guards no longer appear in Zend_object. If magic methods (__set, __get, __isset, __unset) are defined in the class, Guards are stored in the first slot of properties_TABLE; Otherwise guards are not stored.

④ Zend_object_store and zend_object_store_bucket are removed and replaced with a C array of zend_Object Pointers, with handle as the index of the array. In addition, the handlers that were previously stored in the bucket are now moved into the Zend_object; In the previous bucket, dtor, free_storege, and Clone are now moved to Zend_object_handlers.

struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                     offset;
    /* general object functions */
    zend_object_free_obj_t                  free_obj;
    zend_object_dtor_obj_t                  dtor_obj;
    zend_object_clone_obj_t                 clone_obj;
    /* individual object functions */
    / /... The rest is the same as in PHP 5
};
Copy the code

⒉ Change of the low-level custom object (PHP extension uses custom object)

/*PHP 5 custom_object*/
struct custom_object {
    zend_object std;
    my_custom_type *my_buffer;
    // ...
};

/*PHP 7 custom_object*/
struct custom_object {
    my_custom_type *my_buffer;
    // ...
    zend_object std;
};
Copy the code

Because PHP 7 uses the struct hack feature in zend_object to keep zend_object memory continuous, the zend_object in the custom object can only be placed at the end. Only zend_object can be stored in zval. In order to resolve the custom_object smoothly through Zend_object, offset is recorded in the Handlers of zend_object.