类扩展或接口如何工作?

时间:2013-03-28 17:43:02

标签: php class opcode

已经遇到过这么多次,我不知道为什么会让我感到好奇。有些类在声明之前有效,有些则没有;

示例1

$test = new TestClass(); // top of class
class TestClass {
    function __construct() {
        var_dump(__METHOD__);
    }
}

输出

 string 'TestClass::__construct' (length=22)

示例2

当一个类扩展另一个类或实现任何接口时

$test = new TestClass(); // top of class
class TestClass implements JsonSerializable {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return json_encode(rand(1, 10));
    }
}

输出

Fatal error: Class 'TestClass' not found 

示例3

让我们尝试上面的同一个课程,但改变位置

class TestClass implements JsonSerializable {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return json_encode(rand(1, 10));
    }
}

$test = new TestClass(); // move this from top to bottom 

输出

 string 'TestClass::__construct' (length=22)

示例4(我也使用class_exists进行了测试)

var_dump(class_exists("TestClass")); //true
class TestClass {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return null;
    }
}

var_dump(class_exists("TestClass")); //true

一旦实施JsonSerializable(或任何其他)

var_dump(class_exists("TestClass")); //false
class TestClass implements JsonSerializable {

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return null;
    }
}

var_dump(class_exists("TestClass")); //true

同时检查了操作码without JsonSerializable

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   SEND_VAL                                                 'TestClass'
         1      DO_FCALL                                      1  $0      'class_exists'
         2      SEND_VAR_NO_REF                               6          $0
         3      DO_FCALL                                      1          'var_dump'
   4     4      NOP                                                      
  14     5    > RETURN                                                   1

同时检查了操作码with JsonSerializable

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   SEND_VAL                                                 'TestClass'
         1      DO_FCALL                                      1  $0      'class_exists'
         2      SEND_VAR_NO_REF                               6          $0
         3      DO_FCALL                                      1          'var_dump'
   4     4      ZEND_DECLARE_CLASS                               $2      '%00testclass%2Fin%2FaDRGC0x7f563932f041', 'testclass'
         5      ZEND_ADD_INTERFACE                                       $2, 'JsonSerializable'
  13     6      ZEND_VERIFY_ABSTRACT_CLASS                               $2
  14     7    > RETURN                                                   1

问题

  • 我知道Example 3是有效的,因为该类是在它开始之前声明的,但为什么Example 1会在第一时间起作用?
  • 扩展或界面的整个过程如何在PHP中使一个有效,另一个无效?
  • 例4中究竟发生了什么?
  • Opcodes应该让事情变得清晰,但只是因为class_existsTestClass之前被调用而变得更加复杂,但事实恰恰相反。

2 个答案:

答案 0 :(得分:18)

我找不到关于PHP类定义的文章;但是,我想它与实验所表明的User-defined functions完全相同。

在引用函数之前,无需定义函数, 除了 ,当有条件地定义函数时,如下面的两个示例所示。当以条件方式定义函数时;其定义必须在 之前 处理才能被调用。

<?php

$makefoo = true;

/* We can't call foo() from here 
   since it doesn't exist yet,
   but we can call bar() */

bar();

if ($makefoo) {
  function foo()
  {
    echo "I don't exist until program execution reaches me.\n";
  }
}

/* Now we can safely call foo()
   since $makefoo evaluated to true */

if ($makefoo) foo();

function bar() 
{
  echo "I exist immediately upon program start.\n";
}

?>

对于类也是如此:

  • 示例1 有效,因为该课程不以任何其他条件为条件。
  • 示例2 失败,因为该课程以JsonSerializable为条件。
  • 示例3 有效,因为在调用之前已正确定义了类。
  • 示例4 第一次获取为false,因为该类是有条件的,但稍后会成功,因为该类已被加载。

通过实现接口或从另一个文件(require)扩展另一个类来使类成为条件。我把它称为条件因为定义现在依赖于另一个定义。

想象一下PHP解释器首先看一下这个文件中的代码。它看到一个非条件类和/或函数,因此它继续并将它们加载到内存中。它会看到一些有条件的并跳过它们。

然后解释器开始解析页面以供执行。在示例4中,它进入class_exists("TestClass")指令,检查内存,并说nope,我没有。如果没有它,因为它是有条件的。它继续执行指令,查看条件类并执行指令以将类实际加载到内存中。

然后它下降到最后class_exists("TestClass")并看到该类确实存在于内存中。

在阅读您的操作码时,TestClass之前不会调用class_exist。您看到的是发送 TestClass的SEND_VAL,以便它在内存中用于下一行,实际上在class_exists上调用DO_FCALL

然后,您可以看到它如何处理类定义本身:

  1. ZEND_DECLARE_CLASS - 这是加载您的班级定义
  2. ZEND_ADD_INTERFACE - 这会抓取JsonSerializable并将其添加到您的班级定义中
  3. ZEND_VERIFY_ABSTRACT_CLASS - 这证明一切都很清醒。
  4. 第二部分 ZEND_ADD_INTERFACE 似乎阻止PHP引擎仅在其初始峰值上加载类。

      

    如果您希望更详细地讨论PHP解释器的方式   编译并执行这些场景中的代码,我建议采取一个   看看@StasM answer to this question,他   比这个答案更深入地提供了一个很好的概述。

    我认为我们回答了你所有的问题。

    最佳做法:将您的每个课程放在自己的文件中,然后根据需要autoload将其放入其中,@StasM在其答案中说明,使用合理的文件命名和自动加载策略 - 例如 PSR-0 或类似的东西。执行此操作时,您不再需要关注引擎加载它们的顺序,它只是自动为您处理。

答案 1 :(得分:5)

基本前提是,对于要使用的类,必须定义它,即引擎已知。这永远不会改变 - 如果你需要某个类的对象,PHP引擎需要知道这个类是什么。

然而,发动机获得这种知识的那一刻可能会有所不同。首先,引擎消耗PHP代码包含两个独立的进程 - 编译和执行。在编译阶段,引擎将您知道的PHP代码转换为操作码集(您已经熟悉),在第二阶段,引擎通过操作码处理器将通过内存中的指令并执行它们。

其中一个操作码是定义一个新类的操作码,该类通常插入到类定义在源中的相同位置。

但是,当编译器遇到类定义时,它可能能够在执行任何代码之前将类输入到引擎已知的类列表中。这称为“早期绑定”。如果编译器确定它已经具有创建类定义所需的所有信息,并且没有理由将类创建推迟到实际运行时,则会发生这种情况。目前,引擎仅在类:

时执行此操作
  1. 没有附加接口或特征
  2. 不是抽象的
  3. 要么不扩展任何类,要么只扩展引擎已知的类
  4. 被声明为顶级陈述(即不在条件,功能等内)
  5. 此行为也可以通过编译器选项进行修改,但这些行为仅适用于像APC这样的扩展,因此除非您要开发APC或类似的扩展,否则不应该引起您的关注。

    这也意味着没关系:

     class B extends A {}
     class A { }
    

    但这不会是:

     class C extends B {}
     class B extends A {}
     class A { }
    

    由于A是早期绑定的,因此可用于B的定义,但B仅在第2行中定义,因此不适用于第1行的C定义。

    在你的情况下,当你的类实现了接口时,它不是早期绑定的,因此在达到“class”语句时就被引擎所知。当它是没有接口的简单类时,它是早期绑定的,因此一旦文件编译完成就会被引擎知道(你可以在文件中的第一个语句之前看到这一点)。

    为了不打扰引擎的所有这些奇怪的细节,我会支持前一个答案的建议 - 如果你的脚本很小,只需在使用前声明类。如果您有更大的应用程序,请在单个文件中定义您的类,并具有合理的文件命名和自动加载策略 - 例如PSR-0或类似的东西,在你的情况下是合适的。