在PHP中处理include / require指令

时间:2011-10-29 20:07:45

标签: php include

背景 我正在为PHP应用程序构建一个自动化测试框架,我需要一种方法来有效地"stub out"类封装与外部系统的通信。例如,当测试使用DB包装器类Y的类X时,我希望能够在类X上运行自动化测试时“交换”类Y的“假”版本(这样我就不必这样做了)完全设置+拆除真实数据库的状态作为测试的一部分。)

问题: 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()仅包含定义的代码对象,直到“返回”,那么“php B.inc”应该抱怨如下消息:

Fatal error: Class 'Z' not found

问题: 任何人都可以在这里解释完全 PHP正在做什么吗?它实际上对我很重要,因为我需要一个强大的习惯用于处理包括“模拟”类。

5 个答案:

答案 0 :(得分:2)

根据php.net,如果你使用return语句,它会将执行返回到调用它的脚本。这意味着,require_once将停止执行,但整个脚本将继续运行。另外,php.net上的示例显示,如果在包含的文件中返回变量,则可以执行$foo = require_once('myfile.php');之类的操作,$foo将包含所包含文件中返回的值。如果您未返回任何内容,则$foo1以显示require_once成功。阅读this了解更多示例。

我在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

更新

我在Z.inc中尝试了die();语句,它确实停止了所有执行。因此,如果您包含的某个脚本中有一个die语句,那么您将终止测试。

答案 1 :(得分:1)

好的,所以PHP包含文件中的return语句的行为是将控制权返回给执行中的父项。这意味着在编译阶段可以解析和访问类定义。例如,如果您将上述内容更改为以下

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.phpphp 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给出:

  

致命错误:无法在/Users/masud/z-real.inc中重新声明Z类   第2行

php b.php给出:

  

这是来自z-real.inc的foo()。

当然你可以直接在a.php或b.php中这样做,但是做双重间接可能很有用......

SAID所有这一切,当然这是一个可怕的方法来构建存根用于单元测试或任何其他目的:-) ...但这超出了这个问题的范围所以我将留给你好的设备。

希望这有帮助。

答案 2 :(得分:1)

看起来答案是类声明是编译时的,但重复的类定义错误是在声明类的代码中的运行时。第一次类定义在解析块中时,它立即可供使用;通过提前从包含文件返回,您不会阻止类声明,但是 在抛出错误之前已经退出。

例如,这里有一堆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中测试过。)

答案 3 :(得分:0)

这是我在手册中找到的最接近的东西:

  

如果在包含的文件中定义了函数,则它们可以在主文件中独立使用,如果它们在return()之前或之后。如果文件包含两次,PHP 5会发出致命错误,因为函数已经声明,而PHP 4不会抱怨在return()之后定义的函数。

关于功能,情况也是如此。如果使用PHP 5在A和Z中定义相同的函数(在返回之后),则会出现预期的致命错误。

然而,类似乎回归到PHP 4行为,它不会抱怨返回后定义的函数。对我来说,这似乎是一个错误,但我没有看到文档说明应该在类中发生什么。

答案 4 :(得分:0)

我现在已经考虑了一段时间了,没有人能够指出我对PHP(最多5.3个反向)流程包括的方式的清晰一致的解释。

我的结论是,最好完全避免这个问题,并通过 autoloading 实现对“测试双重”类替换的控制:

spl-autoload-register

换句话说,用require_once()替换每个PHP文件顶部的包含,这需要“引导”一个定义自动加载逻辑的类。在编写自动化测试时,“注入”替代自动加载逻辑,以便在每个测试脚本的顶部“模拟”类。

自然需要花费大量精力来修改现有代码以遵循这种方法,但是这些努力似乎值得提高可测试性并减少代码库中的总行数。