Производный класс, определенный позже в том же файле, «не существует»?

Предположим, у нас есть два php-файла, a.php и b.php. Вот содержимое файла a.php:

<?php // content of a.php class A { } 

И вот содержимое файла b.php

 <?php // content of b.php include dirname(__FILE__) . "/a.php"; echo "A: ", class_exists("A") ? "exists" : "doesn't exist", "\n"; echo "B: ", class_exists("B") ? "exists" : "doesn't exist", "\n"; echo "BA (before): ", class_exists("BA") ? "exists" : "doesn't exist", "\n"; echo "BB: ", class_exists("BB") ? "exists" : "doesn't exist", "\n"; class B { } class BA extends A { } class BB extends B { } echo "BA (after): ", class_exists("BA") ? "exists" : "doesn't exist", "\n"; 

Если вы запустите скрипт b.php, вы получите следующий результат:

 A: exists B: exists BA (before): doesn't exist BB: exists BA (after): exists 

Почему класс BA существует только после определения класса? И почему другие классы существуют еще до их определения? В чем разница? Я бы ожидал иметь общее поведение в обоих случаях … Есть ли способ, которым я мог бы использовать класс BA еще до его определения?

спасибо

Michele

    Отказ от ответственности: я не утверждаю, что понимаю внутреннюю работу Zend. Ниже приводится моя интерпретация источника PHP, которая в значительной степени подпитывается образованными догадками. Хотя я полностью уверен в заключении, терминология или детали могут быть отключены. Я бы хотел услышать от кого-нибудь, кто имеет опыт работы в этой области.

    Расследование

    Из парсера PHP мы видим, что когда встречается объявление класса, zend_do_early_binding функция zend_do_early_binding . Вот код, который обрабатывает объявление производных классов:

     case ZEND_DECLARE_INHERITED_CLASS: { zend_op *fetch_class_opline = opline-1; zval *parent_name; zend_class_entry **pce; parent_name = &CONSTANT(fetch_class_opline->op2.constant); if ((zend_lookup_class(Z_STRVAL_P(parent_name), Z_STRLEN_P(parent_name), &pce TSRMLS_CC) == FAILURE) || ((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) && ((*pce)->type == ZEND_INTERNAL_CLASS))) { if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) { zend_uint *opline_num = &CG(active_op_array)->early_binding; while (*opline_num != -1) { opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num; } *opline_num = opline - CG(active_op_array)->opcodes; opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED; opline->result_type = IS_UNUSED; opline->result.opline_num = -1; } return; } if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) { return; } /* clear unnecessary ZEND_FETCH_CLASS opcode */ zend_del_literal(CG(active_op_array), fetch_class_opline->op2.constant); MAKE_NOP(fetch_class_opline); table = CG(class_table); break; } 

    Этот код сразу вызывает zend_lookup_class чтобы узнать, существует ли родительский класс в таблице символов … и затем расходится в зависимости от того, найден ли родитель или нет.

    Давайте сначала посмотрим, что он делает, если найден родительский класс:

     if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) { return; } 

    Переходя к do_bind_inherited_class , мы видим, что последний аргумент (который в этом вызове равен 1 ) называется compile_time . Это звучит интересно. Что он делает с этим аргументом?

     if (compile_time) { op1 = &CONSTANT_EX(op_array, opline->op1.constant); op2 = &CONSTANT_EX(op_array, opline->op2.constant); } else { op1 = opline->op1.zv; op2 = opline->op2.zv; } found_ce = zend_hash_quick_find(class_table, Z_STRVAL_P(op1), Z_STRLEN_P(op1), Z_HASH_P(op1), (void **) &pce); if (found_ce == FAILURE) { if (!compile_time) { /* If we're in compile time, in practice, it's quite possible * that we'll never reach this class declaration at runtime, * so we shut up about it. This allows the if (!defined('FOO')) { return; } * approach to work. */ zend_error(E_COMPILE_ERROR, "Cannot redeclare class %s", Z_STRVAL_P(op2)); } return NULL; } else { ce = *pce; } 

    Хорошо … поэтому он считывает имена родительских и производных классов либо из статического (с точки зрения PHP-пользователя), либо динамического контекста, в зависимости от состояния compile_time . Затем он пытается найти запись класса («ce») в таблице классов, а если он не найден, то … он возвращается, не делая ничего во время компиляции, но испускает фатальную ошибку во время выполнения .

    Это звучит чрезвычайно важно. Вернемся к zend_do_early_binding . Что он делает, если родительский класс не найден?

     if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) { zend_uint *opline_num = &CG(active_op_array)->early_binding; while (*opline_num != -1) { opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num; } *opline_num = opline - CG(active_op_array)->opcodes; opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED; opline->result_type = IS_UNUSED; opline->result.opline_num = -1; } return; 

    Кажется, что он генерирует коды операций, которые снова вызовут do_bind_inherited_class но на этот раз значение compile_time будет равно 0 (false).

    Наконец, что касается реализации class_exists PHP class_exists ? Глядя на источник, вы видите этот фрагмент:

     found = zend_hash_find(EG(class_table), name, len+1, (void **) &ce); 

    Большой! Эта переменная class_table является той же самой class_table которая участвует в вызове do_bind_inherited_class мы видели ранее! Таким образом, возвращаемое значение class_exists зависит от того, была ли запись для класса уже вставлена ​​в class_table by do_bind_inherited_class .

    Выводы

    Компилятор Zend не выполняет директивы include во время компиляции (даже если имя файла жестко запрограммировано).

    Если бы это было так, то не было бы причин испускать факультативную ошибку повторной compile_time класса, основанную на не установленном флаге compile_time ; ошибка может быть испущена безоговорочно.

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

    Это видно из последнего фрагмента кода выше, который устанавливает код операции ZEND_DECLARE_INHERITED_CLASS_DELAYED для регистрации класса при выполнении сценария. В этот момент флаг compile_time будет false а поведение будет немного отличаться.

    Возвращаемое значение class_exists зависит от того, был ли класс уже зарегистрирован.

    Поскольку это происходит по-разному во время компиляции и во время выполнения, поведение class_exists также отличается:

    • классы, чьи предки все включены в один и тот же исходный файл, регистрируются во время компиляции; они существуют и могут быть созданы в любой точке этого скрипта
    • классы, которые имеют предка, определенные в другом исходном файле, регистрируются во время выполнения; прежде чем VM выполнит коды операций, которые соответствуют определению класса в источнике, эти классы не существуют для всех практических целей ( class_exists возвращает false , экземпляр дает фатальную ошибку)

    Это просто, как для класса PHP handle во включенных файлах, include dirname(__FILE__) . "/a.php"; include dirname(__FILE__) . "/a.php";

    BB существует, потому что он расширяет B который был определен в том же файле.

    BA не существует, потому что PHP не анализировал он-лайн он называется

    Оба будут работать, возвращая тот же результат

    Использование class BA extends B

     include dirname(__FILE__) . "/a.php"; echo "BA (before): ", class_exists("BA") ? "exists" : "doesn't exist", "\n"; echo "BB: ", class_exists("BB") ? "exists" : "doesn't exist", "\n"; class B { } class BA extends B { } class BB extends B { } echo "BA (after): ", class_exists("BA") ? "exists" : "doesn't exist", "\n"; 

    Или Определение class A и использование class BA extends A

     class A { } echo "<pre>"; echo "BA (before): ", class_exists("BA") ? "exists" : "doesn't exist", "\n"; echo "BB: ", class_exists("BB") ? "exists" : "doesn't exist", "\n"; class B { } class BA extends A { } class BB extends B { } echo "BA (after): ", class_exists("BA") ? "exists" : "doesn't exist", "\n"; 

    Вывод

     BA (before): exists BB: exists BA (after): exists 

    Вывод

    FORM PHP DOC

    Когда файл включен, код, который он содержит, наследует область изменения строки, в которой происходит включение. Любые переменные, доступные в этой строке в вызывающем файле, будут доступны в вызываемом файле с этой точки вперед. Однако все функции и классы, определенные во включенном файле, имеют глобальную область видимости.

    Я думаю, что расширенные классы охватываются тем, что говорит PHP-документ, это можно рассматривать как BUG, ​​который нужно исправить, но в основное время включите ваш класс, прежде чем вы вызовете или используете их