gpt4 book ai didi

php - 具有依赖项的可测试 Controller

转载 作者:行者123 更新时间:2023-12-02 14:41:16 25 4
gpt4 key购买 nike

如何解决对可测试 Controller 的依赖关系?

工作原理:URI被路由到Controller,Controller可能具有依赖关系来执行特定任务。

<?php

require 'vendor/autoload.php';

/*
* Registry
* Singleton
* Tight coupling
* Testable?
*/

$request = new Example\Http\Request();

Example\Dependency\Registry::getInstance()->set('request', $request);

$controller = new Example\Controller\RegistryController();

$controller->indexAction();

/*
* Service Locator
*
* Testable? Hard!
*
*/

$request = new Example\Http\Request();

$serviceLocator = new Example\Dependency\ServiceLocator();

$serviceLocator->set('request', $request);

$controller = new Example\Controller\ServiceLocatorController($serviceLocator);

$controller->indexAction();

/*
* Poor Man
*
* Testable? Yes!
* Pain in the ass to create with many dependencies, and how do we know specifically what dependencies a controller needs
* during creation?
* A solution is the Factory, but you would still need to manually add every dependencies a specific controller needs
* etc.
*
*/

$request = new Example\Http\Request();

$controller = new Example\Controller\PoorManController($request);

$controller->indexAction();

这是我对设计模式示例的解释

注册表:
  • 单例
  • 紧耦合
  • 可测试?没有

  • 服务定位器
  • 可测试?硬/否(?)

  • 可怜的人
  • 可测试的
  • 难以维护,且具有许多依赖性

  • 登记处
    <?php
    namespace Example\Dependency;

    class Registry
    {
    protected $items;

    public static function getInstance()
    {
    static $instance = null;
    if (null === $instance) {
    $instance = new static();
    }

    return $instance;
    }

    public function set($name, $item)
    {
    $this->items[$name] = $item;
    }

    public function get($name)
    {
    return $this->items[$name];
    }
    }

    服务定位器
    <?php
    namespace Example\Dependency;

    class ServiceLocator
    {
    protected $items;

    public function set($name, $item)
    {
    $this->items[$name] = $item;
    }

    public function get($name)
    {
    return $this->items[$name];
    }
    }

    如何解决对可测试 Controller 的依赖关系?

    最佳答案

    您在 Controller 中谈论的依赖关系是什么?

    到主要解决方案是:

  • 通过构造函数
  • 在 Controller 中注入(inject)服务工厂
  • 使用DI容器直接传递特定服务

  • 我将尝试分别详细描述这两种方法。

    Note: all examples will be leaving out interaction with view, handling of authorization, dealing with dependencies of service factory and other specifics



    注塑厂

    bootstrap 阶段的 简化的部分,负责向 Controller 启动内容,看起来像这样
    $request = //... we do something to initialize and route this 
    $resource = $request->getParameter('controller');
    $command = $request->getMethod() . $request->getParameter('action');

    $factory = new ServiceFactory;
    if ( class_exists( $resource ) ) {
    $controller = new $resource( $factory );
    $controller->{$command}( $request );
    } else {
    // do something, because requesting non-existing thing
    }

    该方法提供了一种简单的方法,只需通过传入不同的工厂作为依赖项,即可扩展和/或替换与模型层相关的代码。在 Controller 中,它看起来像这样:
    public function __construct( $factory )
    {
    $this->serviceFactory = $factory;
    }


    public function postLogin( $request )
    {
    $authentication = $this->serviceFactory->create( 'Authentication' );
    $authentication->login(
    $request->getParameter('username'),
    $request->getParameter('password')
    );
    }

    这意味着,要测试该 Controller 的方法,您将必须编写一个单元测试,该单元测试可以模拟 $this->serviceFactory的内容,创建的实例以及 $request的传入值。所述模拟将需要返回一个实例,该实例可以接受两个参数。

    Note: The response to the user should be handled entirely by view instance, since creating the response is part of UI logic. Keep in mind that HTTP Location header is also a form of response.



    此类 Controller 的单元测试如下所示:
    public function test_if_Posting_of_Login_Works()
    {
    // setting up mocks for the seam

    $service = $this->getMock( 'Services\Authentication', ['login']);
    $service->expects( $this->once() )
    ->method( 'login' )
    ->with( $this->equalTo('foo'),
    $this->equalTo('bar') );

    $factory = $this->getMock( 'ServiceFactory', ['create']);
    $factory->expects( $this->once() )
    ->method( 'create' )
    ->with( $this->equalTo('Authentication'))
    ->will( $this->returnValue( $service ) );

    $request = $this->getMock( 'Request', ['getParameter']);
    $request->expects( $this->exactly(2) )
    ->method( 'getParameter' )
    ->will( $this->onConsecutiveCalls( 'foo', 'bar' ) );

    // test itself

    $instance = new SomeController( $factory );
    $instance->postLogin( $request );

    // done
    }

    Controller 应该是应用程序中最薄的部分。 Controller 的职责是:接受用户输入,并基于该输入更改模型层的状态(在极少数情况下为当前 View )。而已。

    带DI容器

    这另一种方法是..好..基本上,这是一种复杂性的交易(在一处减去,在另一处添加更多)。它还会继续使用 真正的 DI容器,而不是像 Pimple这样的美化服务定位器。

    我的推荐: checkout Auryn

    DI容器的作用是,使用配置文件或反射,它确定要创建的实例的依赖关系。收集所说的依赖关系。并传入实例的构造函数。
    $request = //... we do something to initialize and route this 
    $resource = $request->getParameter('controller');
    $command = $request->getMethod() . $request->getParameter('action');

    $container = new DIContainer;
    try {
    $controller = $container->create( $resource );
    $controller->{$command}( $request );
    } catch ( FubarException $e ) {
    // do something, because requesting non-existing thing
    }

    因此,除了引发异常的能力外, Controller 的引导过程几乎保持不变。

    同样,在这一点上您应该已经意识到,从一种方法切换到另一种方法通常需要完全重写 Controller (以及相关的单元测试)。

    在这种情况下, Controller 的方法如下所示:
    private $authenticationService;

    #IMPORTANT: if you are using reflection-based DI container,
    #then the type-hinting would be MANDATORY
    public function __construct( Service\Authentication $authenticationService )
    {
    $this->authenticationService = $authenticationService;
    }

    public function postLogin( $request )
    {
    $this->authenticatioService->login(
    $request->getParameter('username'),
    $request->getParameter('password')
    );
    }

    至于编写测试,在这种情况下,您所需要做的就是再次提供一些模拟隔离并简单地进行验证。但是,在这种情况下, 单元测试更简单:
    public function test_if_Posting_of_Login_Works()
    {
    // setting up mocks for the seam

    $service = $this->getMock( 'Services\Authentication', ['login']);
    $service->expects( $this->once() )
    ->method( 'login' )
    ->with( $this->equalTo('foo'),
    $this->equalTo('bar') );

    $request = $this->getMock( 'Request', ['getParameter']);
    $request->expects( $this->exactly(2) )
    ->method( 'getParameter' )
    ->will( $this->onConsecutiveCalls( 'foo', 'bar' ) );

    // test itself

    $instance = new SomeController( $service );
    $instance->postLogin( $request );

    // done
    }

    如您所见,在这种情况下,您需要模拟的类要少一些。

    杂记
  • 耦合到名称(在示例中为“身份验证”):

    您可能已经注意到,在两个示例中,您的代码都将与所使用的服务名称耦合在一起。即使使用基于配置的DI容器(可能是in symfony),您仍然最终将定义特定类的名称。
  • DI容器不是魔术:

    在过去的几年中,DI容器的使用已经大肆宣传。这不是 Elixir 。我什至可以说:DI容器与SOLID不兼容。特别是因为它们不适用于接口(interface)。您不能在代码中真正使用多态行为,该行为将由DI容器初始化。

    然后是基于配置的DI的问题。好吧..它只是项目很小的时候的美丽。但是随着项目的增长,配置文件也会增长。您可以得到光荣的xml/yaml配置WALL,项目中只有一个人可以理解。

    第三个问题是复杂性。好的DI容器不易于而不是。而且,如果您使用第三方工具,则会带来其他风险。
  • 依赖性过多:

    如果您的类具有过多的依赖关系,那么按照惯例,DI失败是而不是。相反,它是明确指示,表明您的类(class)在做太多事情。它违反了Single Responsibility Principle
  • Controller 实际上具有(某些)逻辑:

    上面使用的示例非常简单,并且可以通过单个服务与模型层进行交互。在现实世界中,您的 Controller 方法包含控制结构(循环,条件,内容)。

    最基本的用例是使用“主题”下拉列表处理联系表单的 Controller 。大多数消息将被定向到与某些CRM进行通信的服务。但是,如果用户选择“报告错误”,则该消息应传递到差异服务,该服务将在错误跟踪器中自动创建票证并发送一些通知。
  • 这是PHP单元:

    单元测试的示例使用PHPUnit框架编写。如果您使用其他框架或手动编写测试,则必须进行一些基本更改
  • 您将有更多测试:

    单元测试示例不是 Controller 方法要进行的全部测试。特别是当您拥有不平凡的 Controller 时。

  • 其他 Material

    有一些.. emm ...切线主题。

    大括号: 无耻的自我推广
  • dealing with access control in MVC-like architecture

    一些框架具有在 Controller 中推送授权检查的讨厌习惯(不要与“authentication” ..不同的主题混淆)。除了要做完全愚蠢的事情之外,它还引入了 Controller 中的其他依赖项(通常-全局范围)。

    还有另一篇文章使用类似的方法介绍non-invasive logging
  • list of lectures

    它是针对想要学习MVC的人的,但是实际上那里有面向OOP和开发实践的通识教育 Material 。这个想法是,当您完成该列表时,MVC和其他SoC实现将只会使您走到“哦,这有个名字吗?我认为这只是常识。”
  • implementing model layer

    解释上面描述中的那些神奇的“服务”。
  • 关于php - 具有依赖项的可测试 Controller ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20693895/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com