背景 我正在为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正在做什么吗?它实际上对我很重要,因为我需要一个强大的习惯用于处理包括“模拟”类。
答案 0 :(得分:2)
根据php.net,如果你使用return
语句,它会将执行返回到调用它的脚本。这意味着,require_once
将停止执行,但整个脚本将继续运行。另外,php.net上的示例显示,如果在包含的文件中返回变量,则可以执行$foo = require_once('myfile.php');
之类的操作,$foo
将包含所包含文件中返回的值。如果您未返回任何内容,则$foo
为1
以显示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.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
给出:
致命错误:无法在/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 实现对“测试双重”类替换的控制:
换句话说,用require_once()替换每个PHP文件顶部的包含,这需要“引导”一个定义自动加载逻辑的类。在编写自动化测试时,“注入”替代自动加载逻辑,以便在每个测试脚本的顶部“模拟”类。
自然需要花费大量精力来修改现有代码以遵循这种方法,但是这些努力似乎值得提高可测试性并减少代码库中的总行数。