第6章 面向对象

216 阅读9分钟

面向对象编程(Object Oriented Programming, OOP)是一种计算机编程规范。OOP抽象出对象和类的概念——计算机程序由多个独立对象交互组合而成。

本章会分别从类和对象的源码来分析PHP7中面向对象的实现方式。6.1节和6.2节先回顾一下PHP中类的语法及其特性;6.3节分析类的实现;6.4节会对对象进行详细的阐述;6.5节展开介绍魔术方法和自动加载的实现。相信读者通过本章的阅读,能够对PHP 7面向对象的实现有深入的理解。

6.1 类的种类

PHP 7中支持多种类(class)的实现,包括普通类、抽象类、接口、特性以及final类和匿名类。

6.1.1 普通类

类以关键字“class”定义,后跟类名。类名不能是PHP预留关键字,必须以字母或下划线开头,每个类有属于自己的常量、方法、属性。类的实例叫作对象,可通过关键字“new”来实例化对象。

先编写一个PHP的普通类:

<?php
class A{
    const B =1;
    static $b = 2;
    var $c = 3;
    public function b(){
        var_dump("public");
    }
    protected function c(){
        var_dump("protected");
    }
    private function _d(){
        var_dump("private");
    }
    static function e(){
        var_dump(self::$b, self::B);
        //var_dump(self::$c);  //不能通过“::”访问非静态的属性
    }
}
$a = new A();
$a->b();
//$a->c(); //Fatal error: Uncaught Error: Call to protected method A::c() from context ''
//$a->d(); //Fatal error: Uncaught Error: Call to undefined method A::d()
A::e();

说明:

  1. 类的属性和方法有三个访问级别,分别为public(公有), protected(受保护)或private(私有)。类的外部不能直接调用protected(受保护)或private(私有)的方法和属性。
  2. 类的属性有普通属性和静态、常量属性,静态、常量属性则分别用关键字“static”和“const”声明。
  3. 类的普通方法、属性被自己的成员函数调用时可使用“$this->”,静态方法及静态属性也可通过这种方式访问。
  4. 类的常量属性、静态属性、静态方法,通过“self::”调用。非静态的方法也可以通过“::”调用,但是非静态的属性不能通过“::”访问。

6.1.2 抽象类

PHP 5开始支持抽象类和抽象方法,抽象类不能被实例化。

下面编写一个抽象类的示例:

<?php
abstract class A
{
    abstract protected function b($name);
    abstract protected function c();
    // abstract private function c(); /*抽象方法不能设置为私有*/
    abstract  public function d();
    /*PHP Fatal error:  Abstract function A::d() cannot contain body
    abstract  public function d(){}
    */
    public function f(){
        echo 'A->d()';
    }
}
class E extends A{
    public function b($name, $name2=''){
        echo $name.$name2;
    }
    public function c(){
        echo 'pub_c';
    }
    publicfunction d(){
        echo 'pub_d';
    }
    /*访问级别不能降低*/
    /*protected function d(){
        echo 'pri_d';
    } */
    /*参数个数也必须一致*/
    protected function b($name, $name2){
        echo 'pri_d';
    }*/
}
$obj = new E();
$obj->b('name');
$obj->c();
$obj->d();

说明:

  1. 抽象方法必须被子类继承实现,所以不能为私有,只能是受保护的或公有的;
  2. 抽象类的方法访问控制级别必须和子类相等或更为宽松。例如,父类的抽象方法是受保护的,子类实现时则必须为受保护的或者公有的;
  3. 抽象方法的实现,必传参数的数量及类型必须严格一致;
  4. 抽象类的非抽象方法,子类可不实现,等同于普通类方法的继承;
  5. 抽象类中的抽象方法,只能定义,不能实现其方法体;
  6. 抽象类可定义常量,且可被子类覆盖。

6.1.3 接口

PHP与大多数面向对象编程语言一样,不支持多重继承,每个类只能继承一个父类。

对象接口(interface)里定义的方法子类需全部实现,且接口不能直接被实例化。接口的主要特性和需注意的点如下。

  1. 接口类可以通过extend继承一个或多个接口类,多个接口之间用逗号分隔,用以实现接口类的扩充。
  2. 接口类定义的方法必须声明为公有,因此子类的实现方法也只能为公有。接口方法体也必须为空。
  3. 接口类定义的常量和类常量使用方式一样,但不能被子类或者子接口覆盖。
  4. 普通类通过关键字“implements”来实现一个或多个接口。
  5. 继承多个接口,方法不能有重名。
  6. 普通类继承接口,必须实现接口类里面所有的方法,参数也和接口方法定义相同。可加默认参数,这点和抽象类方法的实现基本一致。

接下来,结合PHP代码一起来看接口的定义与实现。

  1. 接口定义
    interface A{
        public function demo1(); /*接口方法不能有方法体,且只能为公有*/
        /* PHP Fatal error:   Access type for interface method A::demo1() must be
          omitted*/
        //protected function demo1();
    }
    
  2. 接口扩充
    interface B{
        public function demo2();
    }
    interface C extends A, B{/*只能通过extends实现接口类扩充*/
        public function demo3();
    }
    
  3. 普通类对接口的实现
    class D  implements A{
        /* PHP Fatal error:  Declaration of D::demo1 ($name) must be compatible with
          A::demo1 ()*/
        //public function demo1($name){
        public function demo1($name=1) {/*接口未定义的参数,可通过加默认值实现*/
        }
    }
    
  4. 普通类实现多个接口
    class D implements A, B, C{/*通过implements实现多个接口类,类之间通过逗号分隔*/
        /*继承的所有接口方法都必须实现,否则报错*/
        public function demo1(){
        }
        public function demo2(){
        }
        public function demo3(){
        }
    }
    

6.1.4 特性

特性(trait)从PHP5.4.0开始启用,便于在不同层次结构内实现代码复用。特性不能直接被实例化,主要特性和需注意的点如下。

  1. 特性与普通类相似,有自己的方法、属性等,但是不可通extends继承,也没有类常量。
  2. 特性的方法如果和当前类方法冲突,会被当前类的方法覆盖。如果和基类方法冲突,特性方法会覆盖基类中的方法。优先级:当前类>特性类>基类。
  3. 一个类加载了多个特性,当多个特性中方法有重名时,需要在代码中通过关键字“insteadof”设置优先级或者通过“as”关键字重命名处理,否则报错。

结合PHP代码实践特性的定义与用法。

  1. 特性的定义
    trait A{                          //定义特性A
        public $b = 'public';         //定义特性的属性
        function demo1(){             //定义特性的普通方法
            echo 'A->demo1';
        }
        static function demo2(){      //定义特性的静态方法
            echo 'A::demo2';
        }
        abstract public function demo3(); //定义特性的抽象方法
    }
    
  2. 特性扩容
    trait B{
        use A;                 //通过关键字use即可拥有特性A全部的方法属性
        function demo4(){
            echo  'B->demo4';
        }
    }
    
    可见,此时B包含了A的所有内容,扩容非常简单。
  3. 普通类使用特性
    class ChildClass {
        use B;                 /*此时相当于把特性B所有方法打包加载进子类*/
        function demo3(){     /*和继承类似,必须实现特性中的抽象方法*/
            echo 'trait:abstract:demo3';
        }
    }
    
  4. 普通类加载多个特性
    trait C{
        function demo5(){
            echo 'C->demo5';
        }
    }
    
  5. 通过use加逗号加载多个特性
    class ChildClass{
        use B, C;       /*逗号隔开,加载多个特性;也可一个一个地加载use B; use C; */
        function demo3(){
            echo 'trait:abstract:demo3';
        }
    }
    
  6. 特性方法优先级(自身>特性>基类)
    class ParentClass{
        function demo1(){     /*被特性A中demo1覆盖*/
            echo 'Parent->demo1';
        }
    }
    class ChildClass extends ParentClass{
        use A;
        function demo2(){     /*自身类demoe2覆盖了特性A中的demo2*/
            echo 'Child->demo2';
        }
        /*避免重复代码,A中抽象方法demo3实现暂时忽略*/
    }
    $obj = new ChildClass();
    $obj ->demo1(); /*A->demo1,特性A的demo1覆盖了基类的demo1*/
    $obj ->demo2(); /*Child->demo2,自身类的demo2覆盖了特性A中demo2*/
    
  7. 优先级冲突解决 当两个特性的方法相同时,需要通过“insteadof”关键字定义谁优先,或通过“as”关键字修改方法名。先定义一个方法与A冲突的特性D:
    trait D{
        function demo1(){
            echo 'C->demo1';
        }
    }
    
    普通类同时加载A与D:
    class ChildClass {//extends  ParentClass {
        //use A, D; /* 方法重名,且没有自定义优先级,则报错:PHP Fatal error:   Trait method
          demo1 has not been applied, because there are collisions with other trait
          methods*/
        //通过insteadof人为地定义优先级,解决冲突,或者通过as给其中一个方法取个别名
        use A, D {
            D::demo1 insteadof A;
            //D::demo1 AS dDemo1;
        }
        /*避免重复代码,A中抽象方法实现暂时忽略*/
    }
    
  8. 修改特性方法的访问控制级别。 在普通类中加载特性,也可通过“as”来修改其访问控制级别,如下示例代码:
    class ChildClass {
            use C {
                    demo5 as private;
              }
    }
    

6.1.5 final类

如果不希望一个类被继承,可以使用“final”来修饰。如果一个方法不想被子类覆盖,也可以这样声明。

结合PHP示例来看。

  1. final修饰的函数不能被覆盖
    class A{
            final function b(){
        }
    }
    class B extends A {
        function b(){//PHP Fatal error:  Cannot override final method A::b()
            echo 'B->b';
        }
    }
    
  2. final修饰的类不能被继承
    final class A{
        function b(){
        }
    }
    class B extends A {// PHP Fatal error:  Class B may not inherit from final class (A)
    }
    

注意

类的属性不能定义为final,只有类和类方法才能被定义为final。


6.1.6 匿名类

当我们想快速实例化一个对象,可以通过匿名类来实现。从PHP 7开始支持匿名类,可通过newclass函数创建,不能有类名。

快速创建一个简单匿名对象:

$obj = new class {
    public function b($msg){
        return $msg;
    }
};
var_dump($obj->b('anonymous')); //输出’anonymous'
var_dump($obj); // object(class@anonymous)#1 (0) {}

6.2 类的特性

上一节介绍了抽象类、匿名类、普通类等的基本特性和简单应用,这一节主要介绍类的特性。

6.2.1 类的属性

类的成员变量叫作属性,属性可声明为private、public、protected三种访问级别。

类的成员方法中,可以通过“$this->”访问非静态、非常量属性;通过“self::”来访问常量及静态属性。接下来逐步介绍各个属性的异同。

  1. 普通属性:普通属性指的是无static、const声明的属性。普通属性通过“->”访问,类实例化成对象后,会把这些属性复制到对象中。
  2. 静态属性:静态属性指的是通过关键字static声明的属性,访问时通过“::”调用。
  3. 常量属性:常量属性指的是通过关键字const声明的属性,常量属性不能被修改,访问时通过“::”调用。
  4. 动态属性:动态属性指的是在程序运行中产生的属性,不是在类中声明的。如以下代码示例:
    class D{
        public function set($param)
        {
            $this->b = $param ;              //设置动态属性
        }
    }
    $obj = new D();
    $obj->set('param_str');
    var_dump($obj->b);
    

6.2.2 访问控制

类中的访问控制是通过在方法和属性前面添加关键字public、protected或private来实现的。


注意

① 在普通类、匿名类和final类中,方法及属性的访问控制声明不受限制,可设为public、protected和private三者中的任意一个;
② 在抽象类和接口中,方法及属性的访问控制不能被声明为private;
③ 在特性类中,方法及属性的访问控制只能被声明为public。

6.3 类的实现

前面介绍了PHP 7的类的种类和常用特性。从本节开始,会依次介绍类在PHP 7中的存储数据结构和类的静态属性、常量、方法、接口和特性的源码实现。最后,以继承的源码实现的分析结束本节。

6.3.1 类的结构

面向对象的核心是类,先来看看PHP 7中存储类的数据结构zend_class_entry:

struct _zend_class_entry {
    char type;
    zend_string *name;
    struct _zend_class_entryparent;
    int refcount;
    uint32_t ce_flags;

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

    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 *__debugInfo;
    union _zend_function *serialize_func;
    union _zend_function *unserialize_func;
    zend_class_iterator_funcs iterator_funcs;

    /* handlers */
    zend_object* (*create_object)(zend_class_entry *class_type);
    zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int
        by_ref);
    int (*interface_gets_implemented)(zend_class_entry  *iface,  zend_class_entry
        *class_type); /* a class implements this interface */
    union _zend_function *(*get_static_method)(zend_class_entryce, zend_stringmethod);

    /* serializer callbacks */
    int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_
        serialize_data *data);
    int (*unserialize)(zval  *object,  zend_class_entry  *ce,  const  unsigned  char
        *buf, size_t buf_len, zend_unserialize_data *data);

    uint32_t num_interfaces;
    uint32_t num_traits;
    zend_class_entry **interfaces;

    zend_class_entry **traits;
    zend_trait_alias **trait_aliases;
    zend_trait_precedence **trait_precedences;

    union {
        struct {
            zend_string *filename;
            uint32_t line_start;
            uint32_t line_end;
            zend_string *doc_comment;
        } user;
        struct {
            const struct _zend_function_entrybuiltin_functions;
            struct _zend_module_entrymodule;
        } internal;
    } info;
};

此结构体的主要字段有以下几个。

  • type:类的类型,共有两种——1代表内置的类,2代表用户自定义的类。

    #define ZEND_INTERNAL_CLASS             //内置类
    #define ZEND_USER_CLASS                 //用户自定义的类
    
  • name:类名。

  • parent:继承的父类指针。

  • refcount:引用计数。

  • ce_flags:位组合标记。其中0x10表示此类有抽象方法,0x20表示此类为抽象类,0x40表示接口,0x80表示特性,0x100表示匿名类,0x400表示其为某个类的子类。 定义如下:

    #define ZEND_ACC_IMPLICIT_ABSTRACT_CLASS    0x10
    #define ZEND_ACC_EXPLICIT_ABSTRACT_CLASS    0x20
    #define ZEND_ACC_INTERFACE                   0x40
    #define ZEND_ACC_TRAIT                       0x80
    #define ZEND_ACC_ANON_CLASS                  0x100
    #define ZEND_ACC_ANON_BOUND                  0x200
    #define ZEND_ACC_INHERITED                   0x400
    
  • default_properties_count:默认普通属性个数。

  • default_static_members_count:默认静态属性个数。

  • default_properties_table:默认普通属性值数组。

  • default_static_members_table:默认静态属性值数组。

  • static_members_table:静态属性成员。

  • constructor:构造方法。

  • destructor:析构方法。

  • clone:克隆方法。

  • __get:魔术方法__get。

  • __set:魔术方法__set。

  • __unset:魔术方法__unset。

  • __isset:魔术方法__isset。

  • __call:魔术方法__call。

  • __callstatic:魔术方法__callstatic。

  • __tostring:魔术方法__tostring。

  • __debugInfo:魔术方法__debugInfo。

  • serialize_func:对象序列化方法。

  • unserialize_func:对象反序列化方法。

  • iterator_funcs:PHP 5开始,支持接口并且内置了Iterator接口,其为此接口的相关操作方法。

  • create_object:实例化对象时调用的方法,默认为函数zend_objects_new,可以通过扩展或修改源码来改变此值。

  • serialize:序列化方法回调指针。

  • unserialize:反序列化方法回调指针。

  • num_interfaces:类implements的接口个数。

  • num_traits:类use的特性个数。

  • interfaces:类implements的接口指针。

  • traits:类use的traits指针。

  • trait_aliases:类use的特性方法的别名。

  • trait_precedences:类use的特性方法的优先级(用于多个特性有相同名称的方法时,解决冲突,6.1.4节中有代码示例)。

  • info:记录类的其他信息,比如类所在的文件、注释之类。结合PHP代码说明特性相关的字段,示例为如下代码:

    trait Win
    {
        public function exec(){
            echo "I am Win! \n";
        }
    }
    trait Mac{
        public function exec(){
            echo "I am Mac! \n";
        }
    }
    trait Config {
        public function filename() {
            echo "php.ini\n";
        }
    }
    
    class Php{
        use Config {
            Config::filename as configName;
        }
        use Win, Mac{
            Win::exec insteadof Mac;
        }
    }
    

上面代码定义了PHP类。

  1. 它用了3个特性(分别为Win、Mac、Config),所以ce->num_traits为3,3个特性的结构体分别为ce->traits[0]、ce->traits[1]、ce->traits[2]。
  2. 代码“Config::filename as configName”添加了一条别名信息,存储在ce->trait_aliases[0]。
  3. 代码“Win::exec insteadof Mac”添加了一条优先级信息,存储在ce->trait_prece-dences[0]。

PHP的类在编译(通过函数zend_compile_class_decl())时生成。每个类对应着一个结构体struct _zend_class_entry,存储在一个以类名字(全部转为小写)为key的HashTable中,也就是全局变量EG(class_table)中。

介绍完了类的存储结构,再来介绍存储属性和方法的相关数据结构zend_property_info:

typedef struct _zend_property_info {
    uint32_t offset; /* property offset for object properties or
        property index for static properties */
    uint32_t flags;
    zend_string *name;
    zend_string *doc_comment;
    zend_class_entry *ce;
} zend_property_info;

各字段含义如下。

  • offset:当查找普通属性时,此值为地址偏移量;查找静态属性时,此值为索引。可能大家很难理解,为什么如此相近的属性,却一个用地址偏移量,另一个用索引?因为普通属性存储在对象结构体struct_zend_object的柔性数组properties_table中(详见6.4.1),而静态属性存储在类结构体struct_zend_class_entry的静态属性指针default_static_members_table指向的内存块中。一个结构体中只能有一个柔性数组,而对象结构中, 只有这一个字段properties_table是变长的数组,所以把此字段放在结构体最后面,用柔性数组即可。而类结构中,有三个字段(default_properties_table、default_static_members_table、static_members_table)是变长数组,所以只能通过指针的方式实现。
  • flags:属性的访问权限以及是否是静态属性。
    #define ZEND_ACC_PUBLIC     0x100
    #define ZEND_ACC_PROTECTED  0x200
    #define ZEND_ACC_PRIVATE    0x400
    #define ZEND_ACC_STATIC     0x01
    
  • name:属性名称。
  • doc_comment:注释。
  • ce:所属的类指针。

仍然以示例说明:

class Php{
    const VERSION_5 = 5;  //
    const VERSION_7 = 7; //
    protected $_version; //
    public function version(){
    return $this->_version;
    }
}
class Php7 extends Php{
    protected $_ast; //
    public function ast(){
        return $this->_ast;
    }
}

PHP通过zend_compile_class_decl()函数将AST树转成struct_zend_class_entry结构体,通过gdb打印这段代码生成的两个struct_zend_class_entry结构。

查看PHP类的名称:

(gdb) p *compiler_globals.class_table.arData[157].val.value.ce.name.val@3
$2 = "Php"

示例代码中PHP类在源码中的数据结构存在的地址如下:

(gdb) p compiler_globals.class_table.arData[157].val.value.ce
$19 = (zend_class_entry *) 0x7ffff7c03018

类的数据结构示意图如图6-1所示。

图6-1 类的数据结构示意图

大家知道,一个类的静态属性和静态方法是和类相关的,而普通属性是和对象相关的,所以下节先详细介绍静态属性、常量和方法。

6.3.2 静态属性、常量和方法

  1. 静态属性:如前文所述,静态属性存储在properties_info和default_static_members_table中。properties_info是一个HashTable,当访问一个静态属性时,以变量名为key,在properties_info中找到对应的value,再取结构体struct _zend_property_info中的字段offset。而default_static_members_table是一个数组,所以default_static_members_table[offset]即为目标属性值。 类的静态属性查找示意图如图6-2所示。

    图6-2 类的静态属性查找示意图

  2. 常量:类常量存储在HashTable类型的constants_table字段中。

  3. 方法:类的方法(包括类的静态方法和类的普通方法)和普通方法(非成员方法)编译后生成的zend_op_array基本没有区别。唯一区别就是类的方法编译后生成的zend_op_array是存在于类结构体的function_table中,不像普通方法,编译后存储在全局变量CG(function_table)中。

类成员方法的访问权限(private、protected、public)以及是否是静态方法等信息,存储在zend_op_array中的fn_flags字段里。

类的普通方法调用与静态方法调用基本无异。区别在于普通方法可以使用this变量获取到当前所在对象。因此,在ZEND_INIT_METHOD_CALL操作中,最后初始化调用栈的时候会将当前对象当成参数this传入。

因此,如果在一个类的普通方法的实现中,没有用到$this变量,那么把普通方法当成静态方法调用是没有问题的,否则会报语法错误。这和C++语言的实现基本上一样。

<? php
class Php{
    protected $_version;
        public function version1(){
        echo "7.1.1\n";
    }
    public function version2(){
        $this->_version = "7.1.0";
        echo $this->_version;
    }
}
Php::version1();
Php::version2();

因为PHP没有指针的概念,为了更加深刻地理解类的普通方法的this变量,可通过一段神奇的C++代码来横向对比一下。

#include<iostream>
using namespace std;
class Php{
    protected:
        std::string _version;
        public:
        void version()
        {
            std::cout << "7.1.0" << endl;
        this->_version = “7.1.0”; //会报错
        }
};
int main(int argc, char* argv[])
{
        Php* php = (Php*)0;
        php->version();
        return 0;
}

上面的代码并没有实例化一个PHP对象,而是直接使用了0这个指针,但通过这个空指针调用这个类的普通方法,程序仍然可以正常运行,和PHP的原理一样,因为我们并没有使用this指针,也没有修改非法内存。

6.3.3 接口和特性

  1. 接口 PHP只支持单一继承,也就是一个子类只能有一个父类。而为了实现类似于C++的多重继承的功能,PHP引入了接口的概念。接下来介绍接口的实现。

    在一个类初始化时,已经确定了此类的接口个数,而关联一个类与其所实现的接口,是通过函数zend_do_implement_interface()来实现的。

    关联接口和类时,根据PHP关于接口的语法,可猜测进行了哪些操作。

    1. ce->num_interfaces加1,将此接口的结构体指针赋值给ce->interfaces[ce->num_interfaces-1]。
    2. 遍历接口中的constants_table,并依次插入到ce->constants_table。如果类和接口有相同名字的常量,则报错。
    3. 遍历接口中的function_table,根据继承的逻辑,判断是否可以插入到类的function_table中。如果可以,则继承此方法。否则不进行任何操作。
    4. 将接口中的interfaces按顺序拷贝到类的interfaces后。
  2. 特性 前文已介绍了特性的具体定义和代码示例,现在来介绍特性的具体实现。特性与类进行关联通过方法zend_do_bind_traits()来实现:

    ZEND_API void zend_do_bind_traits(zend_class_entry *ce) /* {{{ */
    {
        if (ce->num_traits <= 0) {
            return;
            }
        /* complete initialization of trait strutures in ce */
        zend_traits_init_trait_structures(ce);
        /* first care about all methods to be flattened into the class */
        zend_do_traits_method_binding(ce);
        /* Aliases which have not been applied indicate typos/bugs. */
        zend_do_check_for_inconsistent_traits_aliasing(ce);
        /* then flatten the properties into it, to, mostly to notfiy developer about problems */
        zend_do_traits_property_binding(ce);
        /* verify that all abstract methods from traits have been implemented */
        zend_verify_abstract_class(ce);
        /* Emit E_DEPRECATED for PHP 4 constructors */
        zend_check_deprecated_constructor(ce);
        /* now everything should be fine and an added ZEND_ACC_IMPLICIT_ABSTRACT_CLASS
            should be removed */
        if (ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) {
            ce->ce_flags -= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS;
        }
    }
    

主要完成了如下操作。

  1. 完成类结构体指针ce的特性初始化。遍历ce的特性优先级变量ce->trait_precedences(假如遍历中的当前变量叫作cur_pre)。校验优先级最高的类和方法的语法,如果优先级中类cur_pre->exclude_from_classes还未编译,则进行编译(类似include的编译);然后保证优先级中用到的特性类cur_pre->trait_method->ce存在于变量ce->traits中,且优先级特性中的方法cur_pre->trait_method->method_name在特性类的方法表cur_pre->trait_method->ce->function_table中,再遍历被排除的类和方法的变量cur_pre->exclude_from_classes,校验其相关语法。遍历类结构体中的特性别名变量ce->trait_aliases,和上面的逻辑一样,编译未编译的类,校验相关语法。
  2. 将特性的方法拷贝到类中。遍历类结构体中的变量ce->traits,将其方法拷贝到ce->function_table。在拷贝之前执行判断:先将ce->trait_precedences中因为优先级被排除的类的方法排除掉,如果有方法存在别名,则将以别名为新名的方法拷贝到ce->function_table。
  3. 校验别名的相关语法。遍历类结构体中的特性别名变量ce->trait_aliases,如发现仍然有别名所属的类未找到,则抛出语法错误。
  4. 拷贝特性的属性到类中。遍历ce->num_traits,即遍历每个特性的属性properties_info,如果此属性在类中存在,且是继承于父类,则将此属性删除,然后将特性中的属性拷贝到类中;若不是继承自父类,则继续遍历。如果此属性在类中不存在,则将特性中的属性拷贝到类中。
  5. 校验类是否已实现所有抽象方法,没有则报错。
  6. 校验类是否存在与类同名的方法来做构造方法,有则提示。

6.3.4 继承

继承划分了类的层次,父类代表的是更一般、更泛化的类,而子类则更为具体、细化。继承是实现代码重用、扩展软件功能的重要手段。子类中与父类完全相同的属性和方法不必重写,只需写出新增或改写的内容,不必一切从零开始。

PHP只支持单一继承,实现相对简单。PHP父类和子类是分别编译的。编译完成后,再对父类和子类进行继承。继承操作在函数do_bind_inherited_class()中完成。

  1. 继承属性 普通属性和静态属性的继承是先后完成的。在类结构中二者的存储十分相近,继承的操作也十分相近,这里只介绍普通属性继承的实现。

    1. 申请一个元素类型是zval的数组table,大小为父类的普通属性个数(parent_ce->default_properties_count)和子类的普通属性个数(ce->default_properties_count)之和。
    2. 将父类的普通属性中parent_ce->default_properties_table的元素拷贝到数组table。
    3. 将子类的普通属性中的ce->default_properties_table的ce->default_properties_count个元素拷贝到table+parent_ce->default_properties_count。
    4. 释放子类的普通属性指针ce->default_properties_table,将table赋值给ce->default_properties_table。

    这样就完成了普通属性的合并,请看如图6-3所示的示意图。

    图6-3 普通属性的继承

    类的静态属性也如此完成合并。 可以看出,子类的静态属性和普通属性在元素中的位置,相对于合并前都有偏移,所以要对其在HashTable中的偏移进行重置,重置的大致步骤如下。

    1. 遍历properties_info:如果元素是静态属性,则对offset加parent_ce->default_static_members_count。
    2. 如果元素是普通属性,则对offset加parent_ce->default_properties_count * sizeof(val)。
    3. 接下来进行子类的properties_info和父类的properties_info的合并。

    由于不同的属性可能拥有不同的权限,例如父类和子类有重复的属性,甚至重复属性的类型也不同(这里的类型指普通属性和静态属性),所以这两个HashTable的合并会有很复杂的逻辑,但是基于以上讲的数据结构,实现起来并不复杂,对PHP语法特别熟稔的同学,完全可以自己实现这段代码,这里就不啰嗦了。

  2. 继承常量 常量存储是用HashTable实现的,两个HashTable的合并比较简单,无非就是遍历父类的constants_table。

    • 如果子类中存在此常量,则不进行任何操作。
    • 如果子类中不存在此常量,则把此key->val键值对插入到子类的constants_table中。
      if (zend_hash_num_elements(&parent_ce->constants_table)) {
          zend_class_constant *c;
      
          zend_hash_extend(&ce->constants_table,
              zend_hash_num_elements(&ce->constants_table) +
              zend_hash_num_elements(&parent_ce->constants_table), 0);
      
          ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->constants_table, key, c) {
              do_inherit_class_constant(key, c, ce);
          } ZEND_HASH_FOREACH_END();
      }
      
  3. 继承方法 与常量继承的实现类似,方法的继承也是遍历父类的function_table,然后将结果插入到子类的function_table中。不同的是,可能存在方法为private、abstract或final特性,或者同一个方法在父类中为静态方法,而在子类中为普通方法等特殊情况。

    if (zend_hash_num_elements(&parent_ce->function_table)) {
        zend_hash_extend(&ce->function_table,
            zend_hash_num_elements(&ce->function_table) +
            zend_hash_num_elements(&parent_ce->function_table), 0);
        ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->function_table, key, func) {
            zend_function *new_func = do_inherit_method(key, func, ce);
    
            if (new_func) {
                _zend_hash_append_ptr(&ce->function_table, key, new_func);
            }
        } ZEND_HASH_FOREACH_END();
    }
    

6.4 对象的实现

对象是类的实例,上面讲了类的源码实现,接下来讲对象的源码实现,并介绍和类相关的普通属性。

6.4.1 实现

先来看看对象的存储结构:

struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval               properties_table[1];
};

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 */
    zend_object_read_property_t              read_property;
    zend_object_write_property_t             write_property;
    zend_object_read_dimension_t             read_dimension;
    zend_object_write_dimension_t            write_dimension;
    zend_object_get_property_ptr_ptr_t       get_property_ptr_ptr;
    zend_object_get_t                        get;
    zend_object_set_t                        set;
    zend_object_has_property_t               has_property;
    zend_object_unset_property_t             unset_property;
    zend_object_has_dimension_t              has_dimension;
    zend_object_unset_dimension_t            unset_dimension;
    zend_object_get_properties_t             get_properties;
    zend_object_get_method_t                 get_method;
    zend_object_call_method_t                call_method;
    zend_object_get_constructor_t            get_constructor;
    zend_object_get_class_name_t             get_class_name;
    zend_object_compare_t                    compare_objects;
    zend_object_cast_t                       cast_object;
    zend_object_count_elements_t             count_elements;
    zend_object_get_debug_info_t             get_debug_info;
    zend_object_get_closure_t                get_closure;
    zend_object_get_gc_t                     get_gc;
    zend_object_do_operation_t               do_operation;
    zend_object_compare_zvals_t              compare;
};

结构体struct _zend_object各个字段的说明如下。

  • gc:gc头部(详见第3章)。
  • handle:每生成一个结构体zend_object,会将其首地址存储在全局变量executor_globals.objects_store.object_buckets中,而handle即为此结构体在此全局变量中的索引。
  • ce:所属的类结构体指针。
  • handlers:初始化时,默认指向全局变量std_object_handlers,存储着包括操作对象属性等的多个指针函数。
  • properties:HashTable结构,存储对象的动态普通属性值。
  • properties_table:柔性数组,存储对象的普通属性值。在初始化时创建,数组大小为对象所属类的默认普通属性个数default_properties_count+1。

接下来,创建一个对象Php7,其生成的数据结构示意图如图6-4所示。

$obj = new Php7();

图6-4 对象的结构

6.4.2 普通属性

普通属性也存储在对象中。当查找对象的普通属性时,在其所属的类的变量properties_info中,根据属性名key,找到value(类型为zend_property_info结构体),判断属性后,结构体的字段offset即为要查找到的属性值在对象的properties_table中的地址偏移量,如图6-5所示。

看上去顺利地解决了普通属性的存储问题,其实不然。请看下段代码:

$php = new Php7();
$php->filename = "beatles.php";

基于PHP的特性,对象中还存在一种动态普通属性。当然了,由于动态普通属性没有权限问题,也不需要在对象创建时初始化,所以比较简单,直接存储到对象的properties字段就好了。

6.5 其他特性

6.5.1 魔术方法

魔术方法是PHP独有的特性。魔术方法的实现方式与一般的方法几乎无异。区别就是部分魔术方法不只存在于zend_class_entry中的function_table,在zend_class_entry中也会直接存一份,所以会有如图6-6所示的结构图(请注意__get方法的存储位置)。

6.5.2 自动加载

自动加载是依据用户实现的规则来加载PHP文件,实现类的加载。比如现在新建一个PHP 7类型的对象,如果还未加载这个类,且用户没有实现自动加载方法,或是实现的自动加载方法没能把PHP 7这个类加载进来,则会报语法错误。

根据这个逻辑,能迅速猜测到自动加载的实现原理:当用到一个类时,如果在EG(class_table)中没有找到这个类,则去调用用户实现的自动加载方法。调用后到EG(class_table)中查找此类,如果此时仍然没有,则抛出语法错误。

PHP 7中提供了两种自动加载方式:__autoload()和spl_autoload_register()。

  1. __autoload:这个自动加载方法比较简单,在PHP中实现了此方法后,会存储在EG(autoload_func)中。当需要加载新的类时,内核会调用此方法加载类。 此外,内核自己实现了__autoload的默认版本PHP_FUNCTION(spl_autoload)。
  2. spl_autoload_register:这种加载方法比较高级。除了可以实现多个加载逻辑之外,还可以设置优先级(把加载逻辑放在最高优先级或者最低优先级)。知名管理工具Composer便主要是靠spl_autoload_register来实现的。 而且,还可以通过spl_autoload_unregister来删除某个加载逻辑。

现在我们知道,通过spl_autoload_register来进行自动加载,大体有以下几个特征:多个、优先级、可查找(查找后删除)。那么之前学到的内核数据结构有没有可以满足这个需求的?答案是有,内核强大的HashTable完全可以满足这些需求。

事实上,内核确实就是用HashTable来实现的。内核使用HashTable存储在全局变量SPL_G(autoload_functions)中。SPL_G(autoload_functions)初始化为NULL,当程序第一次调用spl_autoload_register()方法来增加自动加载逻辑时,内核会对其进行初始化。

由于spl_autoload_register()可以通过方法、类的普通方法甚至是闭包来实现自动加载,所以这个HashTable的value用zend_function或用zval都不能满足需求,为了满足需求,内核定义了一个结构体autoload_func_info,用于存储用户实现的自动加载逻辑。

typedef struct {
    zend_function *func_ptr;
    zval obj;
    zval closure;
    zend_class_entry *ce;
} autoload_func_info;

可以看出,此value类型并非联合体,实现起来很简单。

此外,需要说明的是,无论实现了哪种自动加载,都是在PHP_FUNCTION(spl_autoload_call)中对自动加载方法进行调用。如果全局变量SPL_G(autoload_functions)已初始化,则按顺序调用加载逻辑,直到此类正确加载。否则直接调用__autoload实现。

if (SPL_G(autoload_functions)) {
    . . .
    while  (zend_hash_get_current_key_ex(SPL_G(autoload_functions),  &func_name,
        &num_idx, &pos) == HASH_KEY_IS_STRING) {
        alfi = zend_hash_get_current_data_ptr_ex(SPL_G(autoload_functions), &pos);
        . . .
        if (zend_hash_exists(EG(class_table), lc_name)) {
            break;
        }
        zend_hash_move_forward_ex(SPL_G(autoload_functions), &pos);
    }
    zend_exception_restore();
    zend_string_free(lc_name);
    SPL_G(autoload_running) = l_autoload_running;
} else {
    /* do not use or overwrite &EG(autoload_func) here */
    zend_call_method_with_1_params(NULL, NULL, NULL, "spl_autoload", NULL, class_
        name);
}

6.6 本章小结

本章介绍了面向对象的相关实现,包括类的静态属性、普通属性、常量方法、特性和接口,以及对象的创建、继承等,最后又介绍了魔法方法和自动加载。读完本章,想必读者对PHP 7面向对象的实现原理已经有了比较清晰的认识。