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

Предположим, у нас есть два файла 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 еще до его определения?

Спасибо

Мишель


person Michele Locati    schedule 27.09.2012    source источник
comment
Какую версию PHP вы используете ?? Не могу воспроизвести эту ошибку   -  person Baba    schedule 27.09.2012
comment
См. абстракцию классов здесь php.net/manual/en/language.oop5.abstract .php   -  person Bud Damyanov    schedule 27.09.2012
comment
Очень хороший вопрос! Возможно, вам следует добавить, что если класс A определен в том же файле, то BA (до) также существует.   -  person Jon    schedule 27.09.2012
comment
@Baba: моя версия php в 5.3.15   -  person Michele Locati    schedule 27.09.2012
comment
@Jon: это причина, по которой я добавил класс BB   -  person Michele Locati    schedule 27.09.2012
comment
@bodi0: я не вижу ничего, связанного с моим вопросом, в php.net/ руководство/en/language.oop5.abstract.php   -  person Michele Locati    schedule 27.09.2012
comment
@MicheleLocati: я только что закончил расследование и добавил ответ. Большое спасибо, что задали такой интересный вопрос!   -  person Jon    schedule 27.09.2012


Ответы (2)


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

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

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

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;

Похоже, он генерирует коды операций, которые инициируют вызов< /a> снова в do_bind_inherited_class -- но на этот раз значение compile_time будет 0 (ложь).

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

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

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

Выводы

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

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

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

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

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

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

  • классы, все предки которых включены в один и тот же исходный файл, регистрируются во время компиляции; они существуют и могут быть созданы в любой момент этого скрипта
  • классы, предок которых определен в другом исходном файле, регистрируются во время выполнения; до того, как виртуальная машина выполнит коды операций, соответствующие определению класса в исходном коде, эти классы не существуют для всех практических целей (class_exists возвращает false, создание экземпляра дает фатальную ошибку)
person Jon    schedule 27.09.2012
comment
Большое спасибо за ваше глубокое исследование. В любом случае, когда вы говорите, что компилятор Zend не работает с директивами включения во время компиляции, я спросил себя, почему. Можем ли мы считать это ошибкой? - person Michele Locati; 27.09.2012
comment
@MicheleLocati: не совсем ИМХО - имя файла может быть переменной, и в этом случае невозможно включить файл во время компиляции. Таким образом, вам понадобятся два отдельных пути, и вы получите непоследовательное поведение, не говоря уже об ошибках, которые могут возникнуть из-за этого изменения в поведении. - person Jon; 27.09.2012
comment
Да, ты прав. Не баг... Но поведение для конечных пользователей все же какое-то глупое. Кстати, большое спасибо! - person Michele Locati; 27.09.2012
comment
При автозагрузке get_class() и экземпляре сначала будет запущен этот, прежде чем произойдет сбой. Обычно автозагрузчик должен иметь возможность загружаться в класс, и оба должны показывать ожидаемое поведение, даже если классы mssing еще не включены (потому что они включали now). Это работает и для классов/интерфейсов, которые также появляются в implements- или extends-операторах. Тем не менее, мне нравится ваше объяснение :) - person KingCrunch; 08.10.2012

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

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

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

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

Использование 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

Заключение

ФОРМА PHP-ДОКУМЕНТА

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

Я думаю, что расширенные классы описаны в том, что говорится в документе PHP, это можно рассматривать как ОШИБКУ, которую необходимо исправить, но в основном включите свой класс, прежде чем вызывать или использовать их.

person Baba    schedule 27.09.2012
comment
Этой ОШИБКЕ уже 7 лет ;) Я думаю, мы можем с уверенностью сказать... классы с extends никогда не будут свободны от этой проблемы. - person IncredibleHat; 06.02.2019