2011-10-29 51 views
1

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

问题: PHP允许“条件包括”,这意味着基本上是包括/要求指令被作为处理的文件的“主”逻辑的一部分,例如处理:

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

问题是我无法弄清楚当包含文件的“main”逻辑调用“return”时会发生什么。包含文件中的所有对象(定义,类,函数等)是否导入到调用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()包括o NLY定义代码对象最多的“回归”,然后选择“PHP B.inc” 应该与这样的消息抱怨:

Fatal error: Class 'Z' not found 

问: 任何人都可以解释究竟什么PHP做, 这里?这对我来说很重要,因为我需要一个强大的成语来处理“嘲笑”类的包含。

+0

首先,我我不确定你想要完成什么。单元测试不应该需要任何hacky定义或条件包含。但是,除此之外,这是一个有趣的双重行为。没有回报,你会得到重复的类错误。不确定手册是否解决这个问题。 – Matthew

+0

@Matthew - 我无法在PHP手册中找到任何解释我在OP中描述的行为(因此是问题)的内容。至于单元测试和黑客攻击包括,我发现在实践中,以某种方式注入模拟对象(又名测试双打)并不是很容易获得超过非常适中的测试覆盖范围,并且由于PHP不会使“猴子修补”非常很容易(比如说Python),那么我不得不在黑客入侵,物理替换文件和取消类层次结构之间作出选择,以实现这一点。 – Peter

回答

0

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

我得出结论,倒不如完全避免这个问题,达到通过autoloading在“测试双”级替代控制:

spl-autoload-register

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

自然需要大量的努力来修改现有的代码才能遵循这种方法,但是这似乎是提高可测试性和减少代码库中行数的有效方法。

+1

另一种方法是使用反转控制框架(如[Dice](http://r.je/dice.html))来照顾自动加载,并使得依赖注入更容易实现,而不会陷入[信使反模式](http://r.je/oop-courier-anti-pattern.html)。 – Peter

2

根据php.net,如果您使用return语句,它会将执行返回给调用它的脚本。这意味着,require_once将停止执行,但整个脚本将继续运行。此外,php.net上的示例显示,如果您在一个包含文件中返回一个变量,那么您可以执行诸如$foo = require_once('myfile.php');之类的操作,并且$foo将包含来自包含文件的返回值。如果您不返回任何内容,则$foo1以表明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

所以,我想在A.inc声明Z类两次,我拿到了致命的错误,但是当我试图在Z.inc两次声明,我没有得到一个错误。这导致我相信php解释器会将执行返回到执行包括在包含文件中发生致命运行时错误时包含的文件。这就是为什么A.inc没有使用Z.inc的类定义。它从未投入环境,因为它造成了致命的错误,将执行返回到A.inc

UPDATE

我试过die();声明Z.inc,和它实际上停止所有执行。因此,如果您的一个包含脚本有die声明,那么您将终止测试。

+0

感谢您的调查,并为“双重申报”实验(提供了新的和有用的信息)。我同意没有重新宣布错误是神秘的。但是我不能相信PHP会在遇到致命错误时从处理include/require处“无声地”返回,因为*任何*致命错误都应该中断脚本的执行(这实际上是您在观察时的行为在Z.inc中放置一个die();语句)。 – Peter

+0

是的,这很奇怪。不幸的是,它看起来像php的源代码调查将是必要的进一步:( – Ryan

+0

+1至少仔细看看,并分享您的意见。 – Peter

0

这是我能找到的手册中最接近的事:

If there are functions defined in the included file, they can be used in the main file independent if they are before return() or after. If the file is included twice, PHP 5 issues fatal error because functions were already declared, while PHP 4 doesn't complain about functions defined after return().

以及有关职能这是真的。如果你用PHP 5在A和Z中定义了相同的函数(在返回之后),你将会得到你所期望的致命错误。

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

+0

感谢您的检查。我同意文件无法澄清什么行为应该这是为什么我确信PHP的上帝会否认这是一个bug – Peter

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(); 

?> 

ž。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"; 
     } 
} 

?> 

现在包含在运行时确定:^)beca直到$z_source值被引擎评估为止,使用该决定是不会做出的。

现在你得到所需的行为,即:

php a.php给出:

Fatal error: Cannot redeclare class Z in /Users/masud/z-real.inc on line 2

php b.php给出:

This is foo() from the z-real.inc.

当然,你可以直接在a.php只会或b做到这一点.php但做双重间接可能会有用...

注意

已经说了所有这些,当然这是一个可怕的方式来建立存根hehe进行单元测试或其他任何目的:-) ......但这超出了这个问题的范围,所以我将离开它给你的好设备。

希望这会有所帮助。

+0

感谢您的调查我同意包含的文件*以某种方式进行检查* - 它们的语法错误确实会导致包含的文件(和脚本)会失败,但是包含的文件是* not *“parsed”,与执行时解析PHP文件的方式相同,如果它们*是*,那么Ryan的答案中描述的“重复定义场景”也会导致包含失败,但事实并非如此。 – Peter

+0

在进一步的研究中,PHP正在展示文档中所表达的行为,即COUNTER。 –

+0

+1用于深入挖掘。您是否想解释观察到的行为与文档相矛盾? – Peter

0

它看起来像答案是类声明是编译时,但重复类定义错误是运行时在代码中该类声明点。类定义第一次在解析块中时,它立即可用;通过尽早从包含文件中返回,您不会阻止类声明,但是您在之前是在抛出错误之前进行救援。

例如,这里有针对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也没有机会抱怨同一文件中的第二个定义,因为它在返回第二个定义之前也会返回。

相反,去除Z.php第一return;而不是生产:

In Z.php! 

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

通过简单地不是从Z.php早回来,我们到达类的声明,其中有一个机会来生产其运行时错误。

总结:类声明是编译时,但重复的定义错误是类声明出现在代码中的运行时。

(当然,具有不与PHP内部证实了这一点,它可能会做一些完全不同的,但行为与我的描述在PHP 5.5.14以上。测试是一致的。)