badoo/

“软模拟”背后的想法——与在PHP解释器级别(runkit和uopz)工作的“硬核”模拟相对——是在现场重写类代码,以便它可以被插入任何地方。它通过在文件包含期间即时重写代码来工作,而不是使用r这样的扩展。

3.6.0 2024-01-09 15:18 UTC

README

“软模拟”背后的想法——与在PHP解释器级别(runkit和uopz)工作的“硬核”模拟相对——是在现场重写类代码,以便它可以被插入任何地方。它通过在文件包含期间即时重写代码来工作,而不是使用r这样的扩展。

Build Status GitHub release Total Downloads Daily Downloads Minimum PHP Version License

安装

您可以通过Composer安装SoftMocks

composer require --dev badoo/soft-mocks

用法

SoftMocks与众不同的地方(也是它们使用的限制)是它们需要在应用启动的最早阶段启动。必须这样做,因为您不能重新定义已加载到PHP内存中的类和函数。有关示例引导预设,请参阅src/bootstrap.php。对于PHPUnit,您应该使用composer.json中的补丁,因为您应该通过SoftMocks要求composer自动加载。

SoftMocks不会重写以下系统部分

  • 自己的代码;
  • PHPUnit代码(有关详细信息,请参阅\Badoo\SoftMocks::addIgnorePath());
  • PHP-Parser代码(有关详细信息,请参阅\Badoo\SoftMocks::addIgnorePath());
  • 已重写的代码;
  • 在SoftMocks初始化之前加载的代码。

为了将外部依赖项(例如,vendor/autoload.php)添加到在SoftMocks初始化之前加载的文件中,您需要使用包装器

require_once (\Badoo\SoftMocks::rewrite('vendor/autoload.php'));
require_once (\Badoo\SoftMocks::rewrite('path/to/external/lib.php'));

通过SoftMocks::rewrite()添加文件后,所有嵌套的包含调用都已经由系统本身“包装”。

您可以通过执行以下命令查看更详细的示例

$ php example/run_me.php
Result before applying SoftMocks = array (
  'TEST_CONSTANT_WITH_VALUE_42' => 42,
  'someFunc(2)' => 84,
  'Example::doSmthStatic()' => 42,
  'Example->doSmthDynamic()' => 84,
  'Example::STATIC_DO_SMTH_RESULT' => 42,
)
Result after applying SoftMocks = array (
  'TEST_CONSTANT_WITH_VALUE_42' => 43,
  'someFunc(2)' => 57,
  'Example::doSmthStatic()' => 'Example::doSmthStatic() redefined',
  'Example->doSmthDynamic()' => 'Example->doSmthDynamic() redefined',
  'Example::STATIC_DO_SMTH_RESULT' => 'Example::STATIC_DO_SMTH_RESULT value changed',
)
Result after reverting SoftMocks = array (
  'TEST_CONSTANT_WITH_VALUE_42' => 42,
  'someFunc(2)' => 84,
  'Example::doSmthStatic()' => 42,
  'Example->doSmthDynamic()' => 84,
  'Example::STATIC_DO_SMTH_RESULT' => 42,
)

API(简要描述)

初始化SoftMocks(设置PHPUnit注入、定义内部模拟、获取内部函数列表等)

\Badoo\SoftMocks::init();

默认情况下,缓存文件存储在/tmp/mocks。如果您想选择不同的路径,您可以按以下方式重新定义它

\Badoo\SoftMocks::setMocksCachePath($cache_path);

此方法应在重写第一个文件之前调用。您还可以使用环境变量SOFT_MOCKS_CACHE_PATH重新定义缓存路径。

重新定义常量

您可以给$constantName分配新值或创建一个尚未声明的常量。由于它不是使用define()调用创建的,因此操作可以被取消。

支持“常规常量”和类常量,如“className::CONST_NAME”。

\Badoo\SoftMocks::redefineConstant($constantName, $value)

重新定义类常量时可能存在以下情况

  • 您可以重新定义基类常量
    class A {const NAME = 'A';}
    class B {}
    echo A::NAME . "\n"; // A
    echo B::NAME . "\n"; // A
    \Badoo\SoftMocks::redefineConstant(A::class . '::NAME', 'B');
    echo A::NAME . "\n"; // B
    echo B::NAME . "\n"; // B
  • 您可以添加中间类常量
    class A {const NAME = 'A';}
    class B {}
    class C {}
    echo A::NAME . "\n"; // A
    echo B::NAME . "\n"; // A
    echo C::NAME . "\n"; // A
    \Badoo\SoftMocks::redefineConstant(B::class . '::NAME', 'B');
    echo A::NAME . "\n"; // A
    echo B::NAME . "\n"; // B
    echo C::NAME . "\n"; // B
  • 您可以添加常量到基类
    class A {const NAME = 'A';}
    class B {}
    echo A::NAME . "\n"; // Undefined class constant 'NAME'
    echo B::NAME . "\n"; // Undefined class constant 'NAME'
    \Badoo\SoftMocks::redefineConstant(A::class . '::NAME', 'A');
    echo A::NAME . "\n"; // A
    echo B::NAME . "\n"; // A
  • 您可以删除中间类常量
    class A {const NAME = 'A';}
    class B {const NAME = 'B';}
    class C {}
    echo A::NAME . "\n"; // A
    echo B::NAME . "\n"; // B
    echo C::NAME . "\n"; // B
    \Badoo\SoftMocks::removeConstant(B::class . '::NAME');
    echo A::NAME . "\n"; // A
    echo B::NAME . "\n"; // A
    echo C::NAME . "\n"; // A
  • 其他更简单的情况(只需添加或重新定义常量等)。

重新定义函数

SoftMocks允许您重新定义用户定义的和内建的函数,除了那些依赖于当前上下文(如果您想查看完整的列表,请参阅\Badoo\SoftMocksTraverser::$ignore_functions属性)的函数,或者那些具有内置模拟的函数(debug_backtrace、call_user_func*和少数其他函数,但您可以通过调用\Badoo\SoftMocks::setRewriteInternal(true)来启用内置模拟的重定义)。

定义

\Badoo\SoftMocks::redefineFunction($func, $functionArgs, $fakeCode)

使用示例(重新定义strlen函数并调用原始函数以获取修剪后的字符串)

\Badoo\SoftMocks::redefineFunction(
    'strlen',
    '$a',
    'return \\Badoo\\SoftMocks::callOriginal("strlen", [trim($a)]));'
);

var_dump(strlen("  a  ")); // int(1)

重新定义方法

目前仅支持用户定义的方法重定义。此功能不支持内置类。

定义

\Badoo\SoftMocks::redefineMethod($class, $method, $functionArgs, $fakeCode)

参数与redefineFunction相同,但新增了参数$class。

作为参数,$class可以接受一个类名或特质名。

重定义生成器函数

此方法允许您将生成器函数调用替换为另一个\Generator。生成器与常规函数的不同之处在于,您不能使用"return"返回值;您必须使用"yield"。

\Badoo\SoftMocks::redefineGenerator($class, $method, \Generator $replacement)

恢复值

以下函数可以撤销使用上述重定义方法之一制作的模拟。

\Badoo\SoftMocks::restoreAll()

// You can also undo only chosen mocks:
\Badoo\SoftMocks::restoreConstant($constantName)
\Badoo\SoftMocks::restoreAllConstants()
\Badoo\SoftMocks::restoreFunction($func)
\Badoo\SoftMocks::restoreMethod($class, $method)
\Badoo\SoftMocks::restoreGenerator($class, $method)
\Badoo\SoftMocks::restoreNew()
\Badoo\SoftMocks::restoreAllNew()
\Badoo\SoftMocks::restoreExit()

与PHPUnit一起使用

如果您想使用SoftMocks与PHPUnit 8.x一起使用,那么有以下特殊注意事项

对于phpunit7.x,请使用phpunit7.x目录代替phpunit8.x。对于phpunit6.x,请使用phpunit6.x目录代替phpunit8.x。对于phpunit5.x,请使用phpunit5.x目录代替phpunit8.x。对于phpunit4.x,请使用phpunit4.x目录代替phpunit8.x

如果您希望自动应用补丁,您应该在composer.json中写入以下内容

{
  "require-dev": {
    "vaimo/composer-patches": "3.23.1",
    "phpunit/phpunit": "^8.4.3" // or "^7.5.17" or "^6.5.5" or "^5.7.20" or "^4.8.35"
  }
}

要强制重新应用补丁,请使用以下命令

composer patch --redo

有关补丁的更多信息,请参阅vaimo/composer-patches文档

与xdebug一起使用

有两种方法可以将SoftMocks与xdebug一起使用 - 调试重写的文件和使用xdebug-proxy调试原始文件。

调试重写的文件

如果您在本地使用SoftMocks,则可以通过调用xdebug_break()来调试它。您还可以将断点添加到重写的文件中,但您应该知道重写文件的路径。要获取重写文件的路径,您可以调用\Badoo\SoftMocks::rewrite($file),但请注意 - 如果您更改了文件,则会创建新文件,并且路径将不同。

如果您在服务器上使用SoftMocks,则可以使用sshfs之类的工具挂载/tmp/mocks。

使用xdebug-proxy调试原始文件

如您所见,调试重写的文件不方便。您还可以使用xdebug-proxy调试原始文件。

composer.phar require mougrim/php-xdebug-proxy --dev
cp -r vendor/mougrim/php-xdebug-proxy/config xdebug-proxy-config

之后,将xdebug-proxy-config/factory.php更改如下

<?php
use Mougrim\XdebugProxy\Factory\SoftMocksFactory;

return new SoftMocksFactory();

如果您在本地使用SoftMocks,则只需运行代理

vendor/bin/xdebug-proxy --configs=xdebug-proxy-config

之后,在127.0.0.1:9001上注册您的IDE,并运行使用SoftMocks的脚本(例如phpunit)

php -d'zend_extension=xdebug.so' -d'xdebug.remote_autostart=On' -d'xdebug.idekey=idekey' -d'xdebug.remote_connect_back=On' -d'xdebug.remote_enable=On' -d'xdebug.remote_host=127.0.0.1' -d'xdebug.remote_port=9002' /local/php72/bin/phpunit

如果您在服务器上使用SoftMocks,那么您也应该在服务器上运行xdebug-proxy,并修改xdebug-proxy-config/config.php中的ideRegistrationServer的ip,从127.0.0.1更改为0.0.0.0

总的来说,xdebug-proxy工作如下

  1. 第一步是在xdebug-proxy中注册您的IDE(例如:主菜单 -> 工具 -> DBGp代理 -> 在PHPStorm中注册IDE)。使用xdebug-proxy监听的IDE注册的IP:PORT(例如:127.0.0.1:9001)或您的服务器IP:PORT。您可以在xdebug-proxy配置中配置该端口。在这一步中,IDE将发送其监听的IP:PORT到代理。
  2. 当您使用上述命令行选项运行 php-script 时,xdebug 会连接到 127.0.0.1:9002。这个 IP 和端口号是 xdebug-proxy 监听 xdebug 连接的地方。Xdebug-proxy 会将 IDEKEY 与已注册的 IDE 匹配。如果匹配到已注册的 IDE,则 xdebug-proxy 将在注册步骤使用提供的 IDE 客户端 IP:PORT 连接到该特定 IDE。

有关更多信息,请阅读 xdebug 文档xdebug-proxy 文档

SoftMocks 开发

如果您需要修改 SoftMocks,您需要克隆仓库并安装依赖项

composer install

然后您可以修改 SoftMocks 并运行测试,以确保一切正常

./vendor/bin/phpunit 

常见问题解答

:如何防止特定的函数/类/常量被重新定义?

:使用 \Badoo\SoftMocks::ignore(Class|Function|Constant) 方法。

:我无法覆盖某些函数调用:call_user_func(_array)?,defined 等。

:有一些函数有自己的内置模拟,默认情况下无法拦截。以下是一个不完整的列表

  • call_user_func_array
  • call_user_func
  • is_callable
  • function_exists
  • constant
  • defined
  • debug_backtrace

因此,您可以在 require bootstrap 后调用 \Badoo\SoftMocks::setRewriteInternal(true) 以启用对它们的拦截,但请注意。例如,如果 strlen 和 call_user_func(_array) 被重新定义,那么您可能会得到不同的 strlen 结果

\Badoo\SoftMocks::redefineFunction('call_user_func_array', '', 'return 20;');
\Badoo\SoftMocks::redefineFunction('strlen', '', 'return 5;');
...
strlen('test'); // will return 5
call_user_func_array('strlen', ['test']); // will return 20
call_user_func('strlen', 'test'); // will return 5

:SoftMocks 是否与 PHP7 兼容?

:是的。SoftMocks 的整个想法是它将继续与所有后续的 PHP 版本一起工作,而无需像 runkit 和 uopz 那样进行完整的系统重写。

:SoftMocks 是否与 HHVM 兼容?

:似乎在编写此问答时,使用 HHVM 时 SoftMocks 确实可以工作(HipHop VM 3.12.1(rel))。我们内部没有使用 HHVM,因此可能会有一些未涵盖的边缘情况。我们欢迎任何有关 HHVM 支持的问题/拉取请求。

:为什么我会遇到解析错误或类似“PhpParser::pSmth 未定义”的致命错误?

:SoftMocks 使用自定义的 PHP Parser 打印机,似乎与所有 PHP Parser 版本不兼容。请在我们找到解决方法之前使用我们提供的版本。