Define the same PHP function in many namespaces.

huangapple go评论75阅读模式
英文:

Define the same PHP function in many namespaces

问题

为了在单元测试中创建全局函数的模拟版本,我将其模拟版本定义在SUT类的命名空间中,如此处所解释的那样。

考虑一个全局函数 foo()Bar 命名空间和 Baz 命名空间中都被使用的情况。如果我想在两个地方使用相同的 foo() 模拟版本,似乎我被迫要为 foo() 进行两次声明。

/* bar-namespace-mocks.php */
namespace Bar;
function foo(){
   return 'foo mock';
}
/* baz-namespace-mocks.php */
namespace Baz;
function foo(){
   return 'foo mock';
}

这段代码不符合DRY原则。更好的方式是只声明一次foo模拟版本。

/* foo-mock.php */
function foo(){
   return 'foo mock';
}

然后按需要导入每个命名空间,如以下伪代码所示:

/* namespace-mocks.php */
namespace Bar{
  import 'foo-mock.php';
} 
namespace Baz{
  import 'foo-mock.php';
}

使用include导入,例如

namespace Baz;
include 'foo-mock.php';

不会导致模拟在Baz命名空间中被声明。是否有办法在多个命名空间中声明一个函数而不必多次版本化它?

英文:

In order to create a mock version of a global function for unit testing, I define its mock version in the namespace of the SUT class, as is explained here

Consider a case of global function foo() which is used in namespace Bar and in namespace Baz. If I want to use the same mock version of foo() in both places, it seems I am forced to make two declarations of the mock for foo()

/* bar-namespace-mocks.php */
namespace Bar;
function foo(){
   return 'foo mock';
}
/* baz-namespace-mocks.php */
namespace Baz;
function foo(){
   return 'foo mock';
}

This code does not conform to the DRY principal.
It would be preferred to have one declaration of the foo mock

/* foo-mock.php */
function foo(){
   return 'foo mock';
}

and then import into each namespace as needed like the following pseudo code:

/* namespace-mocks.php */
namespace Bar{
  import 'foo-mock.php';
} 
namespace Baz{
  import 'foo-mock.php';
}

Importing using include, e.g.

namespace Baz;
include 'foo-mock.php'

does not cause the mock to be declared in the Baz namespace. Is there any way to declare a function in more than one namespace without having more than one version of it?

答案1

得分: 1

如果您需要对一个本地函数进行抽象化,那么请为其创建一个契约(contract),以便您可以使用依赖注入来提供其提供的服务:

interface FooInterface
{
    public function get(): string;
}

然后提供一个默认的实现,使用本地函数:

class NativeFoo implements FooInterface
{
    public function get(): string
    {
        return \foo();
    }
}

然后您的主要类可能看起来像这样:

class A
{
    protected FooInterface $foo;

    public function __construct(FooInterface $foo = null)
    {
        $this->foo = $foo ?? new NativeFoo();
    }

    public function whatever()
    {
        $foo = $this->foo->get();
    }
}

因此,在构造函数中有一个可选参数,如果您不提供值,将会得到本地的foo函数作为默认值。您只需执行以下操作:

$a = new A();
$a->whatever();

但如果您想要覆盖它,可以这样做:

$foo = new SomeOtherFoo();
$a = new A($foo);
$a->whatever();

然后,在您的测试中,您可以构建一个显式的模拟:

class MockFoo implements FooInterface
{
    public function get(): string
    {
        return 'some test value';
    }
}

并传递该实例:

$foo = new MockFoo();
$a = new A($foo);
$a->whatever();

或者使用PHPUnit模拟构建器:

$foo = $this->getMockBuilder(FooInterface::class)->... // 具体操作

如果您想要一个更简单的版本,您可以只使用一个可调用对象:

class A
{
    protected $foo;

    public function __construct(callable $foo = null)
    {
        $this->foo = $foo ?? 'foo';
    }

    public function whatever()
    {
        $foo = call_user_func($this->foo);
    }
}

// 默认用法
$a = new A();
$a->whatever();

// 测试用法
$a = new A(fn() => 'some other value');
$a->whatever();
英文:

If you need to abstract away a native function, then make a contract for it, so that you can use dependency injection for the service it provides:

interface FooInterface
{
    public function get(): string;
}

Then provide a default implementation that uses the native function:

class NativeFoo implements FooInterface
{
    public function get(): string
    {
        return \foo();
    }
}

Then your main class might look something like:

class A
{
    protected FooInterface $foo;

    public function __construct(FooInterface $foo = null)
    {
        $this->foo = $foo ?? new NativeFoo();
    }

    public function whatever()
    {
        $foo = $this->foo->get();
    }
}

So, you've got an optional argument in the constructor, and if you don't provide a value, you'll get the native foo function as a default. You'd just do:

$a = new A();
$a->whatever();

But if you want to override that, you can do:

$foo = new SomeOtherFoo();
$a = new A($foo);
$a->whatever();

Then, in your test, you can build an explicit mock:

class MockFoo implements FooInterface
{
    public function get(): string
    {
        return 'some test value';
    }
}

And pass in instance of that:

$foo = new MockFoo();
$a = new A($foo);
$a->whatever();

Or use the PHPUnit mock builder:

$foo = $this->getMockBuilder(FooInterface::class)->... // whatever

If you want a simpler version, you could just use a callable:

class A
{
    protected $foo;

    public function __construct(callable $foo = null)
    {
        $this->foo = $foo ?? 'foo';
    }

    public function whatever()
    {
        $foo = call_user_func($this->foo);
    }
}


// default usage
$a = new A();
$a->whatever();

// test usage
$a = new A(fn() => 'some other value');
$a->whatever();

答案2

得分: 0

I'll provide the translation of the code portion you provided:

在我全心全意同意Alex Howansky的答案的同时,如果你真的执意要使用纯函数,那么你总是可以:

```lang-php
namespace Mocks {
    function foo() {
        return 'foo';
    }
}

namespace Bar {
  function foo() {
      return \Mocks\foo();
  }
}

namespace Baz {
  function foo() {
      return \Mocks\foo();
  }
}

namespace asdf {
    var_dump(\Bar\foo(), \Baz\foo());
}

输出:

string(3) "foo"
string(3) "foo"

尽管在这一点上,我们实际上只是在重新发明类,但更糟糕。 在某个时候,你会想知道如何给变量命名空间,这是不可能的,所以你将在那一点上将其整合到类中。

类、接口、特性/继承将是最合适的解决这个问题的方式。


<details>
<summary>英文:</summary>

While I whole-heartedly agree with Alex Howansky&#39;s answer, if you&#39;re truly hell-bent on using plain functions, then you can always:

```lang-php
namespace Mocks {
    function foo() {
        return &#39;foo&#39;;
    }
}

namespace Bar {
  function foo() {
      return \Mocks\foo();
  }
}

namespace Baz {
  function foo() {
      return \Mocks\foo();
  }
}

namespace asdf {
    var_dump(\Bar\foo(), \Baz\foo());
}

Output:

string(3) &quot;foo&quot;
string(3) &quot;foo&quot;

Though at this point we're really just on our way to reinventing classes, but worse. At some point you're going to wonder how to namespace a variable, and that is simply not possible, so you'll be rolling this into classes at that point.

Classes, interfaces, and traits/inheritance would be most properly leveraged to solve this problem.

huangapple
  • 本文由 发表于 2023年6月15日 01:45:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/76476271.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定