成员方法从本质上来讲也是一种函数,所以其存储结构也和常规函数一样,存储在zend_function结构体中。 对于一个类的多个成员方法,它是以HashTable的数据结构存储了多个zend_function结构体。 和前面的成员变量一样,在类声明时成员方法也通过调用zend_initialize_class_data方法,初始化了整个方法列表所在的HashTable。 在类中我们如果要定义一个成员方法,格式如下:

class Tipi{
    public function t() {
        echo 1;
    }
}

除去访问控制关键字,一个成员方法和常规函数是一样的,从语法解析中调用的函数一样(都是zend_do_begin_function_declaration函数), 但是其调用的参数有一些不同,第三个参数is_method,成员方法的赋值为1,表示它作为成员方法的属性。 在这个函数中会有一系统的编译判断,比如在接口中不能声明私有的成员方法。 看这样一段代码:

interface Ifce {
   private function method();
}

如果直接运行,程序会报错:Fatal error: Access type for interface method Ifce::method() must be omitted in 这段代码对应到zend_do_begin_function_declaration函数中的代码,如下:

if (is_method) {
    if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
        if ((Z_LVAL(fn_flags_znode->u.constant) & ~(ZEND_ACC_STATIC|ZEND_ACC_PUBLIC))) {
            zend_error(E_COMPILE_ERROR, "Access type for interface method %s::%s() must be omitted",
                CG(active_class_entry)->name, function_name->u.constant.value.str.val);
        }
        Z_LVAL(fn_flags_znode->u.constant) |= ZEND_ACC_ABSTRACT; /* propagates to the rest of the parser */
    }
    fn_flags = Z_LVAL(fn_flags_znode->u.constant); /* must be done *after* the above check */
} else {
    fn_flags = 0;
}

在此程序判断后,程序将方法直接添加到类结构的function_talbe字段,在此之后,又是若干的编译检测。 比如接口的一些魔术方法不能被设置为非公有,不能被设置为static,如__call()、__callStatic()、__get()等。 如果在接口中设置了静态方法,如下定义的一个接口:

interface ifce {
    public static function __get();
}

若运行这段代码,则会显示Warning:Warning: The magic method __get() must have public visibility and cannot be static in

这段编译检测在zend_do_begin_function_declaration函数中对应的源码如下:

if (CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE) {
        if ((name_len == sizeof(ZEND_CALL_FUNC_NAME)-1) && (!memcmp(lcname, ZEND_CALL_FUNC_NAME, sizeof(ZEND_CALL_FUNC_NAME)-1))) {
            if (fn_flags & ((ZEND_ACC_PPP_MASK | ZEND_ACC_STATIC) ^ ZEND_ACC_PUBLIC)) {
                zend_error(E_WARNING, "The magic method __call() must have public visibility and cannot be static");
            }
        } else if() {   //  其它魔术方法的编译检测
        }
}

同样,对于类中的这些魔术方法,也有同样的限制,如果在类中定义了静态的魔术方法,则显示警告。如下代码:

class Tipi {
    public static function __get($var) {

    }
}

运行这段代码,则会显示: Warning: The magic method __get() must have public visibility and cannot be static in

与成员变量一样,成员方法也有一个返回所有成员方法的函数–get_class_methods()。 此函数返回由指定的类中定义的方法名所组成的数组。 从 PHP 4.0.6 开始,可以指定对象本身来代替指定的类名。 它属于PHP内建函数,整个程序流程就是一个遍历类成员方法列表,判断是否为符合条件的方法, 如果是,则将这个方法作为一个元素添加到返回数组中。

静态成员方法

类的静态成员方法通常也叫做类方法。 与静态成员变量不同,静态成员方法与成员方法都存储在类结构的function_table 字段。

类的静态成员方法可以通过类名直接访问。

class Tipi{
    public static function t() {
        echo 1;
    }
}

Tipi::t();

以上的代码在VLD扩展下生成的部分中间代码如如下:

number of ops:  8
compiled vars:  none
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   EXT_STMT
         1      NOP
   8     2      EXT_STMT
         3      ZEND_INIT_STATIC_METHOD_CALL                             'Tipi','t'
         4      EXT_FCALL_BEGIN
         5      DO_FCALL_BY_NAME                              0
         6      EXT_FCALL_END
   9     7    > RETURN                                                   1

branch: #  0; line:     2-    9; sop:     0; eop:     7
path #1: 0,
Class Tipi:
Function t:
Finding entry points
Branch analysis from position: 0

从以上的内容可以看出整个静态成员方法的调用是一个先查找方法,再调用的过程。 而对于调用操作,对应的中间代码为 ZEND_INIT_STATIC_METHOD_CALL。由于类名和方法名都是常量, 于是我们可以知道中间代码对应的函数是ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER。 在这个函数中,它会首先调用zend_fetch_class函数,通过类名在EG(class_table)中查找类,然后再执行静态方法的获取方法。

if (ce->get_static_method) {
    EX(fbc) = ce->get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC);
} else {
    EX(fbc) = zend_std_get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC);
}

如果类结构中的get_static_method方法存在,则调用此方法,如果不存在,则调用zend_std_get_static_method。 在PHP的源码中get_static_method方法一般都是NULL,这里我们重点查看zend_std_get_static_method函数。 此函数会查找ce->function_table列表,在查找到方法后检查方法的访问控制权限,如果不允许访问,则报错,否则返回函数结构体。 关于访问控制,我们在后面的小节中说明。

静态方法和实例方法的小漏洞

细心的读者应该注意到前面提到静态方法和实例方法都是保存在类结构体zend_class_entry.function_table中,那这样的话, Zend引擎在调用的时候是怎么区分这两类方法的,比如我们静态调用实例方法或者实例调用静态方法会怎么样呢?

可能一般人不会这么做,不过笔者有一次错误的这样调用了,而代码没有出现任何问题, 在review代码的时候意外发现笔者像实例方法那样调用的静态方法,而什么问题都没有发生(没有报错)。 在理论上这种情况是不应发生的,类似这这样的情况在PHP中是非常的多的,例如前面提到的create_function方法返回的伪匿名方法, 后面介绍访问控制时还会介绍访问控制的一些瑕疵,PHP在现实中通常采用Quick and Dirty的方式来实现功能和解决问题, 这一点和Ruby完整的面向对象形成鲜明的对比。我们先看一个例子:

<?php

error_reporting(E_ALL);

class A {
    public static function staticFunc() {
        echo "static";
    }

    public function instanceFunc() {
        echo "instance";    
    }
}

A::instanceFunc(); // instance
$a = new A();
$a->staticFunc();  // static
?>

上面的代码静态的调用了实例方法,程序输出了instance,实例调用静态方法也会正确输出static,这说明这两种方法本质上并没有却别。 唯一不同的是他们被调用的上下文环境,例如通过实例方法调用方法则上下文中将会有$this这个特殊变量,而在静态调用中将无法使用$this变量。

不过实际上Zend引擎是考虑过这个问题的,将error_reporting的级别增加E_STRICT,将会出出现E_STRICT错误:

Strict Standards: Non-static method A::instanceFunc() should not be called statically

这只是不建议将实例方法静态调用,而对于实例调用静态方法没有出现E_STRICT错误,有人说:某些事情可以做并不代表我们要这样做。

PHP在实现新功能时通常采用渐进的方式,保证兼容性,在具体实现上通常采用打补丁的方式,这样就造成有些”边界“情况没有照顾到。