【问题标题】:Processing include/require directives in PHP在 PHP 中处理 include/require 指令
【发布时间】:2011-10-29 20:07:45
【问题描述】:

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

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

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

问题是我无法弄清楚当包含文件的“主”逻辑调用“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() 只包含定义的代码对象直到“返回”,那么“php B.inc”应该抱怨如下消息:

Fatal error: Class 'Z' not found

问题: 谁能在这里解释准确 PHP 正在做什么?这对我来说实际上很重要,因为我需要一个强大的习惯用法来处理“模拟”类的包含。

【问题讨论】:

  • 首先,我不确定您要完成什么。单元测试不应该需要任何 hacky 定义或条件包含。但除此之外,这是有趣的双重行为。没有返回,你会得到一个重复的类错误。不确定手册是否解决了这个问题。
  • @Matthew - 我无法在 PHP 手册中找到任何解释我在我的 OP 中描述的行为的内容(因此是问题)。至于单元测试和黑客攻击,我发现在实践中,如果不以某种方式注入模拟对象(又名测试替身),要获得比非常适度的测试覆盖率还远非易事,而且由于 PHP 不会非常“打猴子补丁”简单(比如 Python),然后我不得不在 hacking 包含、物理替换文件和颠覆类层次结构之间做出选择以实现这一目标。

标签: php include


【解决方案1】:

根据 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.

显然,这里发生了名称提升,但剩下的问题是为什么没有关于重新声明的投诉?

更新

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

更新

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

【讨论】:

  • 感谢您的调查以及“双重声明”实验(提供了新的有用信息)。我同意缺少重新声明错误是神秘的。但我无法相信 PHP 在遇到致命错误时会“默默地”从处理包含/要求中返回,因为 any 致命错误应该会中断脚本的执行(这实际上是您在 Z.inc 中放置 die(); 语句时观察到的行为。
  • 是的,很奇怪。不幸的是,看起来有必要进一步调查 php 源代码 :(
  • +1 至少仔细查看并分享您的观察结果。
【解决方案2】:

好的,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 中做这件事,但是做双重间接可能会有用...

注意

说了这么多,当然这是一种糟糕的方式来构建存根用于单元测试或任何其他目的:-) ...但这超出了这个问题的范围,所以我将把它留给你好设备。

希望这会有所帮助。

【讨论】:

  • 感谢您的调查。我同意所包含的文件以某种方式检查 - 其中的语法错误确实会导致包含(和脚本)失败。但是包含的文件没有“解析”,就像 PHP 文件在执行时被解析一样。如果他们,那么 Ryan 的回答中描述的“重复定义场景”也会导致包含失败,但它不会。
  • 经过进一步研究,PHP 表现出的行为与文档中的表述相反。
  • +1 用于深入挖掘。您愿意解释观察到的行为与文档有何矛盾吗?
  • 关于你关于构建存根的评论,我将向你重复我写给 Matthew 的内容:我发现在实践中,在不注入 mock 的情况下获得比非常适度的测试覆盖率远非易事对象(又名测试替身)不知何故,并且由于 PHP 并没有使“猴子补丁”变得非常容易(比如 Python),所以我不得不在黑客包含、物理替换文件和颠覆类层次结构之间做出选择,以便实现这一目标。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    这是我在手册中能找到的最接近的内容:

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

    关于函数也是如此。如果你用 PHP 5 在 A 和 Z 中定义相同的函数(返回之后),你会得到一个如你所料的致命错误。

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

    【讨论】:

    • 感谢您的检查。我同意文档未能阐明行为应该是什么,这就是为什么我确信 PHP 之神会否认这是一个错误。
    【解决方案5】:

    我已经考虑了一段时间,但没有人能够指出我对 PHP(无论如何最高 5.3)处理方式的清晰一致的解释。

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

    spl-autoload-register

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

    按照这种方法修改现有代码自然需要付出大量努力,但无论是提高可测试性还是减少代码库中的总行数,这些努力似乎都是值得的。

    【讨论】:

    • 另一种方法是使用控制反转框架(如Dice)来处理自动加载并使依赖注入更容易实现,而不会陷入the courier anti-pattern的困境。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-02-11
    • 1970-01-01
    • 2010-09-25
    • 2016-07-03
    • 2011-04-07
    • 1970-01-01
    • 2017-05-07
    相关资源
    最近更新 更多