用于功能驱动的应用程序的PHP DI模式

时间:2018-05-04 08:51:01

标签: php unit-testing dependency-injection containers

我有一些类需要将依赖项注入到它们的构造函数中。这允许我注入模拟(例如来自prophecy)进行测试。

我有兴趣使用容器来帮助配置和访问这些对象,我已经查看了Pimple(我也看了PHP-DI虽然我无法理解快速尝试解决问题。)

到目前为止一切顺利。 但是,我遇到的问题是应用程序(Drupal 7)是围绕数千个函数构建的,这些函数不属于可以注入依赖项的对象。

所以我需要这些函数才能从容器访问服务。此外,出于测试目的,我需要用模拟和新模拟替换服务。

所以模式就像:

<?php
/**
  * Some controller class that uses an injected mailing service.
  */
class Supporter
{
  protected $mailer;

  public function __construct(MailingServiceInterface $mailer) {
     $this->mailer = $mailer;
  }

  public function signUpForMalings($supporter_id) {
     $email = $this->getSupporterEmail($supporter_id);
     $this->mailer->signup($email);
  }
}

然后在我使用的各种功能中添加:

<?php
/**
  * A form submit handler called by the platform app,
  * with a signature I can't touch.
  */
function my_form_submit($values) {     
  global $container;

  if ($values['subscribe']) {
    $supporter = $container->get('supporter');
    $supporter->signUpForMailings($values['supporter_id']);
  }
}

在其他地方我可能需要直接访问邮件...

<?php
/**
  * example function requires mailer service.
  */
function is_signed_up($email) {
   global $container;
   return $container->get('mailer')->isSignedUp($email);
}

在其他地方调用这些函数的函数......

<?php
/**
  * example function that uses both the above functions
  */
function sign_em_up($email, $supporter_id) {
  if (!is_signed_up($email)) {
    my_form_submit(['supporter_id'=>$supporter_id);
    return TRUE;
  }
}

让我们承认这些功能是一团糟 - 这是故意代表的问题。但是,我想说我想测试sign_em_up函数:

 <?php
public testSignUpNewPerson() {
   $mock_mailer = createAMockMailer()
     ->thatWill()
     ->return(FALSE)
     ->whenFunctionCalled('isSignedUp', 'wilma@example.com');

   // Somehow install the mock malier in the container.

   $result = sign_em_up('wilma@example.com', 123);
   $this->assertTrue($result);
}

// ... imagine other tests which also need to inject mocks.

虽然我认识到这是在各种全局函数中使用容器作为服务定位器,但我认为鉴于平台的性质,这是不可避免的。如果有更清洁的方式,请告诉我。

但我的主要问题是:

注入模拟存在问题,因为模拟需要针对各种测试进行更改。假设我换掉了邮件服务(在Pimple:$container->offsetUnset('mailer'); $container['mailer'] = $mock_mailer;中),但是如果Pimple已经实例化了supporter服务,那么该服务将拥有旧的,未模仿的邮件程序对象。这是包含软件或一般容器模式的限制,还是我做错了,还是因为老派以功能为中心的应用程序而变得一团糟?

1 个答案:

答案 0 :(得分:0)

在没有任何其他建议的情况下,这就是我所追求的目标!

容器使用Pimple\Psr11\ServiceLocator

我使用的是Pimple,因此容器的工厂可能看起来像这样

<?php
use Pimple\Container;
use Pimple\Psr11\ServiceLocator;

$container = new Container();
$container['mailer'] = function ($c) { return new SomeMailer(); }
$container['supporters'] = function ($c) {
  // Create a service locator for the 'Supporters' class.
  $services = new ServiceLocator($c, ['mailer']);
  return new Supporter($services);
}

然后,Supporter类现在不是存储对创建容器时从容器中提取的对象的引用,而是从ServiceLocator中获取它们:

<?php
use \Pimple\Psr11\ServiceLocator;
/**
  * Some controller class that uses an injected mailing service.
  */
class Supporter
{
  protected $services;

  public function __construct(ServiceLocator $services) {
     $this->services = $services;
  }

  // This is a convenience function.
  public function __get($prop) {
    if ($prop == 'mailer') {
      return $this->services->get('mailer');
    }
    throw new \InvalidArgumentException("Unknown property '$prop'");
  }

  public function signUpForMalings($supporter_id) {
     $email = $this->getSupporterEmail($supporter_id);
     $this->mailer->signup($email);
  }
}

在各种CMS功能中,我只使用global $container; $mailer = $container['mailer'];,但这意味着在测试中我现在可以模拟任何服务,并且知道所有需要该服务的代码现在都将拥有我的模拟服务。 e.g。

<?php
class SomeTest extends \PHPUnit\Framework\TestCase
{
   function testSupporterGetsMailed() {
      global $container;

      $supporter = $container['supporter'];

      // e.g. mock the mailer component
      $container->offsetUnset('mailer');
      $container['mailer'] = $this->getMockedMailer();

      // Do something with supporter.
      $supporter->doSomething();

      // ...
   }
}