Обработка include / require директив в PHP - PullRequest
1 голос
/ 30 октября 2011

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

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

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 ]

2 голосов
/ 30 октября 2011

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

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

UPDATE

Я также добавил несколько тестов, изменив 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.

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

UPDATE

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

UPDATE

Я попробовал оператор die(); в Z.inc, и он фактически останавливает все выполнение. Итак, если один из ваших включенных сценариев имеет оператор die, то вы прекратите свое тестирование.

1 голос
/ 14 августа 2015

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

Например, вот несколько определений классов для 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.)

1 голос
/ 30 октября 2011

Хорошо, поэтому поведение оператора 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, но двойное косвенное обращение может быть полезным ...

NOTE

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

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

0 голосов
/ 04 марта 2012

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

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

УЗД-автозагрузка регистра

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

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

0 голосов
/ 30 октября 2011

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...