Обработка 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, где он не жалуется на функции, определенные после возврата. Мне это кажется ошибкой, но я не вижу, где документация говорит, что должно происходить с классами.