Подтвердить что ты не робот

Как работает расширение класса или интерфейс?

Нашли это так много раз, и я не уверен, почему так мне стало любопытно. Некоторые классы работают до того, как они объявлены, а другие - нет;

Пример 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

Также проверенные Opcodes 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

Также проверено Opcodes 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_exists вызывается до TestClass, но обратное дело.
4b9b3361

Ответ 1

Я не могу найти запись в определениях классов PHP; однако я полагаю, что это точно так же, как Пользовательские функции, которые указывают ваши эксперименты.

Функции не обязательно должны быть определены до их ссылки, кроме, когда функция условно определена, как показано в двух примерах ниже. Когда функция определяется условным образом; его определение должно обрабатываться ранее для вызова.

<?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. Вы видите SEND_VAL, который отправляет значение TestClass так, что он находится в памяти для следующей строки, которая на самом деле вызывает DO_FCALL на class_exists

Затем вы можете увидеть, как он обрабатывает само определение класса:

  • ZEND_DECLARE_CLASS - это загрузка определения вашего класса
  • ZEND_ADD_INTERFACE - это извлекает JsonSerializable и добавляет это к вашему определению класса
  • ZEND_VERIFY_ABSTRACT_CLASS - это проверяет, что все в порядке.

Это вторая часть ZEND_ADD_INTERFACE, которая, как представляется, не позволяет PHP Engine просто загружать класс на начальном пике на нем.

Если вы хотите более подробное обсуждение того, как интерпретатор PHP Компилирует и выполняет код в этих сценариях, я предлагаю взять посмотрите @StasM ответ на этот вопрос, он дает отличный обзор этого в большей степени, чем этот ответ.

Думаю, мы ответили на все ваши вопросы.

Лучшая практика: Поместите каждый из ваших классов в свой собственный файл, а затем autoload при необходимости, поскольку @StasM заявляет в своем ответе, используйте разумную стратегию именования файлов и автозагрузки - например PSR-0 или что-то подобное. Когда вы это делаете, вам больше не нужно беспокоиться о том, как загружает их Двигатель, он просто обрабатывает это для вас автоматически.

Ответ 2

Основная предпосылка заключается в том, что для используемого класса он должен быть определен, то есть известен движку. Это никогда не может быть изменено - если вам нужен объект какого-либо класса, PHP-движок должен знать, что такое класс.

Однако момент, когда двигатель получает такие знания, может быть другим. Прежде всего, потребление PHP-кода движком состоит из двух отдельных процессов - компиляции и исполнения. На этапе компиляции движок преобразует PHP-код, как вам известно, в набор кодов операций (которые вы уже знаете), на втором этапе движок просматривает коды операций, поскольку процессор будет выполнять инструкции в памяти и исполняет их.

Один из кодов операций - это код операции, который определяет новый класс, который обычно вставляется в том же месте, где определение класса находится в источнике.

Однако, когда компилятор встречает определение класса, он может ввести класс в список классов, известных движку, перед выполнением какого-либо кода. Это называется "ранним связыванием". Это может произойти, если компилятор решит, что у него уже есть вся информация, необходимая ему для создания определения класса, и нет причин откладывать создание класса до фактического времени выполнения. В настоящее время движок делает это только в том случае, если класс:

  • не имеет интерфейсов или признаков, прикрепленных к нему
  • не является абстрактным
  • либо не распространяет никаких классов, либо расширяет только класс, который уже известен движку
  • объявляется как верхний оператор (т.е. не внутреннее условие, функция и т.д.)

Это поведение также может быть изменено с помощью параметров компилятора, но они доступны только для расширений, таких как APC, поэтому не стоит беспокоиться о вас, если вы не собираетесь разрабатывать APC или подобное расширение.

Это также означает, что это будет нормально:

 class B extends A {}
 class A { }

но это не будет:

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

Так как A будет ранней привязкой и, следовательно, доступно для определения B, но B будет определяться только в строке 2 и, следовательно, недоступно для определения строки C. C./

В вашем случае, когда ваш класс реализовал интерфейс, он не был ранним, и, таким образом, он стал известен движку в момент, когда была достигнута инструкция класса. Когда это был простой класс без интерфейсов, он был ранним и стал известен движку, как только компиляция файла была закончена (вы можете видеть эту точку как одну перед первым оператором в файле).

Чтобы не беспокоиться обо всех этих странных деталях движка, я бы поддержал рекомендацию предыдущего ответа - если ваш script мал, просто объявляйте классы перед использованием. Если у вас более широкое приложение, определите свои классы в отдельных файлах и получите разумную стратегию именования файлов и автозагрузки. PSR-0 или что-то подобное, подходящее в вашем случае.