类扩展或接口如何工作?

时间:2022-09-02 12:09:40

Have come across this so many times and am not sure why so it got me curious. Some classes work before they are declared and others don't;

我曾多次遇到过这种情况,但我不知道为什么会这样。有些类在声明之前工作,有些则不工作;

Example 1

示例1

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

Output

输出

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

Example 2

示例2

When a class extends another class or implements any interface

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

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

    function __construct() {
        var_dump(__METHOD__);
    }

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

Output

输出

Fatal error: Class 'TestClass' not found 

Example 3

示例3

Let's try the same class above but change the position

让我们试试上面的类,但是改变位置

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 

Output

输出

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

Example 4 ( I also tested with class_exists )

示例4(我还用class_exist进行了测试)

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

    function __construct() {
        var_dump(__METHOD__);
    }

    public function jsonSerialize() {
        return null;
    }
}

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

as soon as it implements JsonSerializable ( Or any other)

一旦实现了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

Also Checked Opcodes without JsonSerializable

还检查了没有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

Also Checked Opcodes with JsonSerializable

还使用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

Question

问题

  • I know Example 3 worked because the class was declared before its initiated but why would Example 1 work in the first place ?
  • 我知道示例3是有效的,因为类是在初始化之前声明的,但是为什么示例1首先会起作用呢?
  • How does this entire process of extending or interface work in PHP to make one valid and the other invalid?
  • 这个扩展或接口的整个过程如何在PHP中工作,使其中一个有效,另一个无效?
  • What Exactly is happening in Example 4?
  • 在示例4中具体发生了什么?
  • Opcodes was supposed to make things clear but just made it more complex because class_exists was called before TestClass but the reverse is the case.
  • 操作码应该使事情更清楚,但只是使它更复杂,因为class_exist是在TestClass之前调用的,但情况恰恰相反。

2 个解决方案

#1


18  

I can not find a write up on PHP class definitions; however, I imagine it is precisely the same as the User-defined functions which your experiments indicate.

我找不到关于PHP类定义的描述;但是,我认为它与您的实验所表明的用户定义的函数完全相同。

Functions need not be defined before they are referenced, except when a function is conditionally defined as shown in the two examples below. When a function is defined in a conditional manner; its definition must be processed prior to being called.

函数在被引用之前不需要定义,除非有条件地定义一个函数,如下面的两个例子所示。以条件方式定义函数的;它的定义必须在被调用之前进行处理。

<?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";
}

?>

This is true for classes as well:

这也适用于课堂:

  • Example 1 works because the class is not conditional on anything else.
  • 示例1之所以有效,是因为类不受任何其他条件的限制。
  • Example 2 fails because the class is conditional upon JsonSerializable.
  • 示例2失败,因为类是基于JsonSerializable的条件。
  • Example 3 works because the class is correctly defined prior to being called.
  • 示例3工作是因为在调用之前,类的定义是正确的。
  • Example 4 gets false the first time because the class is conditional but succeeds later because the class has been loaded.
  • 示例4第一次出现错误,因为该类是有条件的,但稍后会成功,因为该类已被加载。

The class is made conditional by either implementing an interface or extending another class from another file (require). I'm calling it conditional because the definition now relies upon another definition.

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

Imagine the PHP interpreter takes a first look at the code in this file. It sees a non-conditional class and/or function, so it goes ahead and loads them in memory. It sees a few conditional ones and skips over them.

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

Then the Interpreter begins to parse the page for execution. In example 4, it gets to the class_exists("TestClass") instruction, checks memory, and says nope, I don't have that. If doesn't have it because it was conditional. It continues executing the instructions, see the conditional class and executes the instructions to actually load the class into memory.

然后解释器开始解析页面以便执行。在示例4中,它到达class_exists(“TestClass”)指令,检查内存,然后说不,我没有那个。如果没有,因为它是有条件的。它继续执行指令,查看条件类,并执行指令将类加载到内存中。

Then it drops down to the last class_exists("TestClass") and sees that the class does indeed exist in memory.

然后下降到最后一个class_exist(“TestClass”)并看到这个类确实存在于内存中。

In reading your opcodes, the TestClass doesn't get called before class_exist. What you see is the SEND_VAL which is sending the value TestClass so that it is in memory for the next line, which actually calls DO_FCALL on class_exists

在读取操作码时,TestClass在class_exist之前不会被调用。您看到的是SEND_VAL,它正在发送值TestClass,这样它就在内存中,用于下一行,它实际上在class_exists上调用DO_FCALL

You can then see how it is handling the class definition itself:

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

  1. ZEND_DECLARE_CLASS - this is loading your class definition
  2. ZEND_DECLARE_CLASS——这是在加载类定义。
  3. ZEND_ADD_INTERFACE - this fetches JsonSerializable and adds that to your class defintion
  4. ZEND_ADD_INTERFACE——这将获取JsonSerializable,并将其添加到类定义中
  5. ZEND_VERIFY_ABSTRACT_CLASS - this verifies everything is sane.
  6. ZEND_VERIFY_ABSTRACT_CLASS——这验证了一切都是正常的。

It is that second piece ZEND_ADD_INTERFACE that appears to prevent the PHP Engine from merely loading the class on the initial peak at it.

这是第二个ZEND_ADD_INTERFACE,它似乎阻止PHP引擎仅仅在初始峰值上加载该类。

If you desire a more detailed discussion of how the PHP Interpreter Compiles and Executes the code in these scenarios, I suggest taking a look at @StasM answer to this question, he provides an excellent overview of it in greater depth than this answer goes.

如果您希望更详细地讨论PHP解释器如何在这些场景中编译和执行代码,我建议您查看一下这个问题的@StasM回答,他提供了比这个答案更深入的详细概述。

I think we answered all of your questions.

我想我们已经回答了你所有的问题。

Best Practice: Place each of your classes in it's own file and then autoload them as needed, as @StasM states in his answer, use a sensible file naming and autoloading strategy - e.g. PSR-0 or something similar. When you do this, you no longer have to be concerned with the order of the Engine loading them, it just handles that for you automatically.

最佳实践:将您的每个类放在它自己的文件中,然后根据需要对它们进行自动排序,如@StasM在其回答中所述,使用合理的文件命名和自动排序策略——例如PSR-0或类似的策略。当你这样做的时候,你不再需要关心引擎装载它们的顺序,它会自动为你处理。

#2


5  

The basic premise is that for class to be used it has to be defined, i.e. known to the engine. This can never be changed - if you need an object of some class, the PHP engine needs to know what the class is.

基本的前提是,要使用类,必须定义类,即引擎所知道的类。这一点永远不会改变——如果您需要某个类的对象,PHP引擎需要知道这个类是什么。

However, the moment where the engine gains such knowledge can be different. First of all, consuming of the PHP code by the engine consists of two separate processes - compilation and execution. On compilation stage, the engine converts PHP code as you know it to the set of opcodes (which you are already familiar with), on the second stage the engine goes through the opcodes as processor would go through instructions in memory, and executes them.

然而,引擎获得这些知识的时刻是不同的。首先,引擎使用PHP代码由两个独立的过程组成——编译和执行。在编译阶段,引擎将PHP代码转换为一组操作码(您已经熟悉了),在第二阶段,引擎通过操作码,就像处理器在内存中执行指令一样。

One of the opcodes is the opcode that defines a new class, which is usually inserted in the same place where the class definition is in the source.

其中一个操作码是定义一个新类的操作码,它通常插入到源中类定义的同一个地方。

However, when the compiler encounters class definition, it may be able to enter the class into the list of the classes known to the engine before executing any code. This is called "early binding". This can happen if the compiler decides that it already has all the information it needs to create a class definition, and there's no reason to defer the class creation until the actual runtime. Currently, the engine does this only if the class:

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

  1. has no interfaces or traits attached to it
  2. 它没有附加接口或特性吗
  3. is not abstract
  4. 不是抽象的
  5. either does not extend any classes or extends only the class that is already known to the engine
  6. 要么不扩展任何类,要么只扩展引擎已知的类
  7. is declared as top statement (i.e. not inside condition, function, etc.)
  8. 声明为top语句(即不包含内部条件、函数等)

This behavior can also be modified by compiler options, but those are available only to extensions like APC so should not be a matter of much concern to you unless you are going to develop APC or similar extension.

这种行为也可以通过编译器选项进行修改,但是这些选项只对APC之类的扩展可用,所以您不必太担心,除非您打算开发APC或类似的扩展。

This also means this would be OK:

这也意味着这是可以的:

 class B extends A {}
 class A { }

but this would not be:

但这不会是:

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

Since A would be early bound, and thus available for B's definition, but B would be defined only in line 2 and thus unavailable for line 1's definition of C.

因为A是早期绑定的,因此B的定义是可用的,但是B只在第2行定义,因此第1行定义C是不可用的。

In your case, when your class implemented the interface, it was not early bound, and thus became known to the engine at the point when "class" statement was reached. When it was simple class without interfaces, it was early bound and thus became known to the engine as soon as the file compilation was finished (you can see this point as one before the first statement in the file).

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

In order not to bother with all these weird details of the engine, I would support the recommendation of the previous answer - if your script is small, just declare classes before usage. If you have bigger application, define your classes in individual files, and have sensible file naming and autoloading strategy - e.g. PSR-0 or something similar, as suitable in your case.

为了避免麻烦于引擎中所有这些奇怪的细节,我将支持前一个答案的建议——如果您的脚本很小,只需在使用之前声明类即可。如果您有更大的应用程序,请在单独的文件中定义类,并具有合理的文件命名和自动读策略——例如PSR-0或类似的东西,以适合您的情况。

#1


18  

I can not find a write up on PHP class definitions; however, I imagine it is precisely the same as the User-defined functions which your experiments indicate.

我找不到关于PHP类定义的描述;但是,我认为它与您的实验所表明的用户定义的函数完全相同。

Functions need not be defined before they are referenced, except when a function is conditionally defined as shown in the two examples below. When a function is defined in a conditional manner; its definition must be processed prior to being called.

函数在被引用之前不需要定义,除非有条件地定义一个函数,如下面的两个例子所示。以条件方式定义函数的;它的定义必须在被调用之前进行处理。

<?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";
}

?>

This is true for classes as well:

这也适用于课堂:

  • Example 1 works because the class is not conditional on anything else.
  • 示例1之所以有效,是因为类不受任何其他条件的限制。
  • Example 2 fails because the class is conditional upon JsonSerializable.
  • 示例2失败,因为类是基于JsonSerializable的条件。
  • Example 3 works because the class is correctly defined prior to being called.
  • 示例3工作是因为在调用之前,类的定义是正确的。
  • Example 4 gets false the first time because the class is conditional but succeeds later because the class has been loaded.
  • 示例4第一次出现错误,因为该类是有条件的,但稍后会成功,因为该类已被加载。

The class is made conditional by either implementing an interface or extending another class from another file (require). I'm calling it conditional because the definition now relies upon another definition.

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

Imagine the PHP interpreter takes a first look at the code in this file. It sees a non-conditional class and/or function, so it goes ahead and loads them in memory. It sees a few conditional ones and skips over them.

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

Then the Interpreter begins to parse the page for execution. In example 4, it gets to the class_exists("TestClass") instruction, checks memory, and says nope, I don't have that. If doesn't have it because it was conditional. It continues executing the instructions, see the conditional class and executes the instructions to actually load the class into memory.

然后解释器开始解析页面以便执行。在示例4中,它到达class_exists(“TestClass”)指令,检查内存,然后说不,我没有那个。如果没有,因为它是有条件的。它继续执行指令,查看条件类,并执行指令将类加载到内存中。

Then it drops down to the last class_exists("TestClass") and sees that the class does indeed exist in memory.

然后下降到最后一个class_exist(“TestClass”)并看到这个类确实存在于内存中。

In reading your opcodes, the TestClass doesn't get called before class_exist. What you see is the SEND_VAL which is sending the value TestClass so that it is in memory for the next line, which actually calls DO_FCALL on class_exists

在读取操作码时,TestClass在class_exist之前不会被调用。您看到的是SEND_VAL,它正在发送值TestClass,这样它就在内存中,用于下一行,它实际上在class_exists上调用DO_FCALL

You can then see how it is handling the class definition itself:

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

  1. ZEND_DECLARE_CLASS - this is loading your class definition
  2. ZEND_DECLARE_CLASS——这是在加载类定义。
  3. ZEND_ADD_INTERFACE - this fetches JsonSerializable and adds that to your class defintion
  4. ZEND_ADD_INTERFACE——这将获取JsonSerializable,并将其添加到类定义中
  5. ZEND_VERIFY_ABSTRACT_CLASS - this verifies everything is sane.
  6. ZEND_VERIFY_ABSTRACT_CLASS——这验证了一切都是正常的。

It is that second piece ZEND_ADD_INTERFACE that appears to prevent the PHP Engine from merely loading the class on the initial peak at it.

这是第二个ZEND_ADD_INTERFACE,它似乎阻止PHP引擎仅仅在初始峰值上加载该类。

If you desire a more detailed discussion of how the PHP Interpreter Compiles and Executes the code in these scenarios, I suggest taking a look at @StasM answer to this question, he provides an excellent overview of it in greater depth than this answer goes.

如果您希望更详细地讨论PHP解释器如何在这些场景中编译和执行代码,我建议您查看一下这个问题的@StasM回答,他提供了比这个答案更深入的详细概述。

I think we answered all of your questions.

我想我们已经回答了你所有的问题。

Best Practice: Place each of your classes in it's own file and then autoload them as needed, as @StasM states in his answer, use a sensible file naming and autoloading strategy - e.g. PSR-0 or something similar. When you do this, you no longer have to be concerned with the order of the Engine loading them, it just handles that for you automatically.

最佳实践:将您的每个类放在它自己的文件中,然后根据需要对它们进行自动排序,如@StasM在其回答中所述,使用合理的文件命名和自动排序策略——例如PSR-0或类似的策略。当你这样做的时候,你不再需要关心引擎装载它们的顺序,它会自动为你处理。

#2


5  

The basic premise is that for class to be used it has to be defined, i.e. known to the engine. This can never be changed - if you need an object of some class, the PHP engine needs to know what the class is.

基本的前提是,要使用类,必须定义类,即引擎所知道的类。这一点永远不会改变——如果您需要某个类的对象,PHP引擎需要知道这个类是什么。

However, the moment where the engine gains such knowledge can be different. First of all, consuming of the PHP code by the engine consists of two separate processes - compilation and execution. On compilation stage, the engine converts PHP code as you know it to the set of opcodes (which you are already familiar with), on the second stage the engine goes through the opcodes as processor would go through instructions in memory, and executes them.

然而,引擎获得这些知识的时刻是不同的。首先,引擎使用PHP代码由两个独立的过程组成——编译和执行。在编译阶段,引擎将PHP代码转换为一组操作码(您已经熟悉了),在第二阶段,引擎通过操作码,就像处理器在内存中执行指令一样。

One of the opcodes is the opcode that defines a new class, which is usually inserted in the same place where the class definition is in the source.

其中一个操作码是定义一个新类的操作码,它通常插入到源中类定义的同一个地方。

However, when the compiler encounters class definition, it may be able to enter the class into the list of the classes known to the engine before executing any code. This is called "early binding". This can happen if the compiler decides that it already has all the information it needs to create a class definition, and there's no reason to defer the class creation until the actual runtime. Currently, the engine does this only if the class:

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

  1. has no interfaces or traits attached to it
  2. 它没有附加接口或特性吗
  3. is not abstract
  4. 不是抽象的
  5. either does not extend any classes or extends only the class that is already known to the engine
  6. 要么不扩展任何类,要么只扩展引擎已知的类
  7. is declared as top statement (i.e. not inside condition, function, etc.)
  8. 声明为top语句(即不包含内部条件、函数等)

This behavior can also be modified by compiler options, but those are available only to extensions like APC so should not be a matter of much concern to you unless you are going to develop APC or similar extension.

这种行为也可以通过编译器选项进行修改,但是这些选项只对APC之类的扩展可用,所以您不必太担心,除非您打算开发APC或类似的扩展。

This also means this would be OK:

这也意味着这是可以的:

 class B extends A {}
 class A { }

but this would not be:

但这不会是:

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

Since A would be early bound, and thus available for B's definition, but B would be defined only in line 2 and thus unavailable for line 1's definition of C.

因为A是早期绑定的,因此B的定义是可用的,但是B只在第2行定义,因此第1行定义C是不可用的。

In your case, when your class implemented the interface, it was not early bound, and thus became known to the engine at the point when "class" statement was reached. When it was simple class without interfaces, it was early bound and thus became known to the engine as soon as the file compilation was finished (you can see this point as one before the first statement in the file).

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

In order not to bother with all these weird details of the engine, I would support the recommendation of the previous answer - if your script is small, just declare classes before usage. If you have bigger application, define your classes in individual files, and have sensible file naming and autoloading strategy - e.g. PSR-0 or something similar, as suitable in your case.

为了避免麻烦于引擎中所有这些奇怪的细节,我将支持前一个答案的建议——如果您的脚本很小,只需在使用之前声明类即可。如果您有更大的应用程序,请在单独的文件中定义类,并具有合理的文件命名和自动读策略——例如PSR-0或类似的东西,以适合您的情况。