Обработка include/require директив в PHP

Предпосылки: я создаю автоматизированную среду тестирования для приложения PHP, и мне нужен способ эффективно "заглушить" классы, которые инкапсулируют связь с внешними системами. Например, при тестировании класса X, использующего класс-оболочку БД Y, я хотел бы иметь возможность "заменить" "поддельную" версию класса Y при выполнении автоматических тестов в классе X (таким образом, мне не нужно делать полная настройка + разбор состояния реальной БД как часть теста).

Проблема: PHP допускает "условное включение", что означает, в основном, что директивы include / require обрабатываются как часть обработки "основной" логики файла, например:

if (condition) {
    require_once('path/to/file');
}

Проблема в том, что я не могу понять, что происходит, когда "основная" логика включенного файла вызывает "возврат". Все ли объекты (определения, классы, функции и т. Д.) Во включенном файле импортированы в файл, который вызывает include/require? Или обработка останавливается с возвратом?

Пример: рассмотрим эти три файла:

A.inc

define('MOCK_Z', true);
require_once('Z.inc');
class Z {
    public function foo() {
        print "This is foo() from a local version of class Z.\n";
    }
}
$a = new Z();
$a->foo();

B.inc

define('MOCK_Z', true);
require_once('Z.inc');
$a = new Z();
$a->foo();

Z.inc

if (defined ('MOCK_Z')) {
    return true;
}
class Z {
    function foo() {
        print "This is foo() from the original version of class Z.\n";
    }
}

Я наблюдаю следующее поведение:

$ php A.inc
> This is foo() from a local version of class Z.

$ php B.inc
> This is foo() from the original version of class Z.

Почему это странно: если require_once() включает в себя все определенные объекты кода, то php A.inc должен пожаловаться на сообщение вроде

Fatal error: Cannot redeclare class Z

И если require_once() включает в себя только определенные объекты кода до "return", тогда "php B.inc" должен пожаловаться на такое сообщение:

Fatal error: Class 'Z' not found

Вопрос: Кто-нибудь может объяснить , что именно PHP делает здесь? Это на самом деле важно для меня, потому что мне нужна надежная идиома для обработки включений для "поддельных" классов.

5 ответов

Решение

Некоторое время я думал об этом, и никто не смог указать мне на четкое и последовательное объяснение того, как PHP (в любом случае, до 5.3) включает процессы.

Я пришел к выводу, что было бы лучше полностью избежать этой проблемы и добиться контроля над заменой класса "test double" с помощью автозагрузки:

СПЛ-автозагрузка-регистр

Другими словами, замените include в верхней части каждого PHP-файла на require_once(), который "загружает" класс, который определяет логику автозагрузки. А при написании автоматических тестов "вводите" альтернативную логику автозагрузки для классов, которые нужно "высмеивать" в верхней части каждого сценария тестирования.

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

Согласно php.net, если вы используете return оператор, он вернет выполнение к сценарию, который вызвал его. Что значит, require_once прекратит выполнение, но общий скрипт продолжит работу. Также примеры на php.net показывают, что если вы возвращаете переменную во включенном файле, вы можете сделать что-то вроде $foo = require_once('myfile.php'); а также $foo будет содержать возвращаемое значение из включенного файла. Если вы ничего не вернете, то $foo является 1 чтобы показать это require_once Был успешен. Прочитайте это для большего количества примеров.

И я не вижу на php.net ничего такого, что говорило бы конкретно о том, как интерпретатор php будет анализировать включенные операторы, но ваше тестирование показывает, что оно сначала разрешает определения классов перед выполнением кода в строке.

ОБНОВИТЬ

Я добавил несколько тестов, изменив Z.inc следующее:

    $test = new Z();
    echo $test->foo();
    if (defined ('MOCK_Z')) {
        return true;
    }
    class Z {
        function foo() {
            print "This is foo() from the original version of class Z.\n";
        }
    }

А затем проверяется в командной строке следующим образом:

    %> php A.inc
    => This is foo() from a local version of class Z.
       This is foo() from a local version of class Z.

    %> php B.inc
    => This is foo() from the original version of class Z.
       This is foo() from the original version of class Z.

Очевидно, здесь происходит подмена имен, но остается вопрос: почему нет претензий по поводу повторных деклараций?

ОБНОВИТЬ

Итак, я попытался объявить класс Z дважды в A.inc и я получил фатальную ошибку, но когда я попытался объявить это дважды в Z.incЯ не получил ошибку. Это наводит меня на мысль, что интерпретатор php вернет выполнение в файл, который выполнил включение, когда во включенном файле произойдет фатальная ошибка времени выполнения. Поэтому A.inc не использовал Z.incопределение класса. Он никогда не помещался в среду, потому что вызывал фатальную ошибку, возвращая выполнение обратно A.inc,

ОБНОВИТЬ

Я попробовал die(); заявление в Z.inc, и это на самом деле останавливает все выполнение. Итак, если один из ваших включенных скриптов имеет die Скажите, тогда вы убьете тестирование.

Хорошо, поэтому поведение оператора return во включенных PHP-файлах заключается в возврате управления родительскому элементу во время выполнения. Это означает, что определения классов анализируются и доступны на этапе компиляции. Например, если вы измените вышеуказанное на следующее

a.php:

<?php
define('MOCK_Z', true);

require_once('z.php');

class Z {
    public function foo() {
        print "This is foo() from a local version of class Z in a.php\n";
    }
}

$a = new Z();
$a->foo();

?> 

b.php:

<?php

    define('MOCK_Z', true);
    require_once('z.php');
    $a = new Z();
    $a->foo();

?>

z.php:

<?php

if (defined ('MOCK_Z')) {
    echo "MOCK_Z definition found, returning\n";
    return false;
}

echo "MOCK_Z definition not found defining class Z\n";

class X { syntax error here ; }

class Z {
    function foo() {
        print "This is foo() from the original version of class Z.\n";
    }
}

?>

затем php a.php а также php b.php оба умрут с ошибками синтаксиса; что указывает на то, что возвращаемое поведение не оценивается на этапе компиляции!

Так вот как вы обходите это:

z.php:

<?php

$z_source = "z-real.inc";

if ( defined(MOCK_Z) ) {
    $z_source = "z-mock.inc";
}

include_once($z_source);

?>

z-real.inc:

<?php
class Z {
    function foo() {
            print "This is foo() from the z-real.inc.\n";
        }
}

?>

z-mock.inc:

<?php
class Z {
    function foo() {
            print "This is foo() from the z-mock.inc.\n";
        }
}

?>

Теперь включение определяется во время выполнения:^), потому что решение не принято до $z_source значение оценивается двигателем.

Теперь вы получаете желаемое поведение, а именно:

php a.php дает:

Неустранимая ошибка: невозможно переопределить класс Z в /Users/masud/z-real.inc в строке 2

а также php b.php дает:

Это foo() из z-real.inc.

Конечно, вы можете сделать это прямо в a.php или b.php, но двойное косвенное обращение может быть полезным...

НОТА

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

Надеюсь это поможет.

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

Например, вот несколько определений классов для Z:

$ cat A.php
<?php
error_reporting(-1);

$init_classlist = get_declared_classes();
require_once("Z.php");
var_dump(array_diff(get_declared_classes(), $init_classlist));

class Z {
  function test() {
    print "Modified Z from A.php.\n";
  }
}

$z = new Z();
$z->test();

return;

class Z {
  function test() {
    print "Another Z from A.php.\n";
  }
}


$ cat Z.php
<?php
echo "In Z.php!\n";
return;

class Z {
  function test() {
    print "Original Z.\n";
  }
}

когда A.php называется, производится следующее:

In Z.php!
array(0) {
}
Modified Z from A.php.

Это показывает, что объявленные классы не изменяются при входе Z.php - класс Z уже объявлен A.php дальше вниз по файлу. Тем не мение, Z.php никогда не получит изменения, чтобы жаловаться на дублирующееся определение из-за возврата до объявления класса. Так же, A.php не получает возможности пожаловаться на второе определение в том же файле, поскольку оно также возвращается до достижения второго определения.

И наоборот, удаление первого return; в Z.php вместо этого производит:

In Z.php!

Fatal error: Cannot redeclare class Z in Z.php on line 4

Просто не возвращаясь рано из Z.php мы достигли объявления класса, которое может выдать ошибку во время выполнения.

В итоге: объявление класса выполняется во время компиляции, но повторяющиеся ошибки определения выполняются в тот момент, когда объявление класса появляется в коде.

(Конечно, не подтвердив это с внутренними компонентами PHP, он может делать что-то совершенно иное, но поведение согласуется с моим описанием выше. Протестировано в PHP 5.5.14.)

Это самая близкая вещь, которую я мог найти в руководстве:

Если есть функции, определенные во включаемом файле, они могут использоваться в главном файле независимо, если они находятся до return() или после. Если файл включен дважды, PHP 5 выдает фатальную ошибку, потому что функции уже объявлены, а PHP 4 не жалуется на функции, определенные после return().

И это верно в отношении функций. Если вы определите ту же функцию в A и Z (после возврата) в PHP 5, вы получите фатальную ошибку, как и ожидалось.

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

Другие вопросы по тегам