在PHP中使用依赖注入编写Controller类

时间:2010-02-05 23:29:24

标签: php model-view-controller dependency-injection instantiation

如何解决PHP中编写Controller 类的问题,应该是:

  • 轻松可测试,采用依赖注入,
  • 为最终程序员提供共享对象
  • 提供加载新用户库
  • 的方法

向下看,对于使用依赖注入框架的控制器实例化


问题是,派生的控制器可能使用程序员想要的任何资源(例如框架提供)。如何创建对共享资源(数据库,用户,存储,缓存,帮助程序),用户定义的类或其他库的统一访问?

优雅的解决方案?

我的问题有几种可能的解决方案,但两者都不是一个优雅的

  • 尝试通过构造函数传递所有共享对象? (甚至可以使用10个占位符创建构造函数)
  • 创建 getters,settters ? (膨胀的代码)$controller->setApplication($app)
  • 在共享资源上应用单身User::getInstance()Database::getInstance()
  • 使用依赖注入容器作为控制器内部对象共享的单例?
  • 提供一个全局应用程序单例作为工厂? (这个在php框架中看起来很常用,但它强烈反对DI原则和Demeter定律)

我明白,创建强耦合类是不鼓励和放逐的:)但是我不知道这个范例如何适用于其他程序员(Controller类)的起点,他们应该能够访问它们提供给MVC架构的共享资源。我相信,将控制器类分解为更小的类会以某种方式破坏MVC的实际意义。


依赖注入框架

DI Framework看起来是一个可行的选择。但问题仍然存在。类似Controller的类不在Application层中,而是在RequestHandler / Response层中。

该图层应如何实例化控制器?

  • 将DI喷射器传递到这一层?
  • DI Framework作为单身人士?
  • 仅为此图层添加隔离的DI框架配置并创建单独的DI注入器实例?

5 个答案:

答案 0 :(得分:3)

您是自己开发框架吗?如果没有,则您的问题不适用,因为您必须从already existing frameworks及其现有解决方案中进行选择。在这种情况下,你的问题必须重新制定,如“如何在框架X中进行单元测试/依赖注入”。

如果您正在自己开发框架,则应首先检查现有框架是如何解决此问题的。您还必须详细说明自己的要求,然后再使用最简单的解决方案。没有要求,你的问题纯粹是审美和争论。

Imho最简单的解决方案是将公共属性初始化为框架提供的默认值,否则可以在此处注入模拟。 (这相当于你的getter / setter解决方案,但是没有提到的膨胀。你并不总是需要getter和setter。)或者,如果你真的需要它,你可以提供一个构造函数来在一次调用中初始化它们(如你所建议的那样)

单身人士是一个优雅的解决方案,但你必须问自己,它是否适用于你的情况?如果您的应用程序中必须有相同类型对象的不同实例,则不能使用它(例如,如果您希望仅在应用程序的一半中模拟一个类)。

当然,拥有所有选项真的很棒。您可以使用getter / setter,构造函数,并且在省略初始化时,默认值来自单件工厂。但是在不需要时有太多选项并不是很棒,因为程序员必须弄清楚要使用哪种约定,选项和模式,这是令人不安的。我绝对不想做出几十个设计决定只是为了让一个简单的CRUD运行。

如果你看看其他框架,你会发现没有银弹。通常,单个框架根据上下文使用不同的技术。在控制器中,DI是一个非常简单的事情,看看CakePHP的$ helpers,$ components变量,它们指示将适当的变量注入控制器类。对于应用程序本身而言,单例仍然是一件好事,因为总是只有一个应用程序。使用公共属性注入较少经常更改/模拟的属性。 对于MVC,子类化也是完全可行的选项:就像CakePHP中的AppController,AppView,AppModel一样。它们被插入到框架和所有特定Controller,View和Model类之间的类层次结构中。通过这种方式,您只需要为主要类类型声明全局变量。

在Java中,由于动态类加载器和反射,您可以选择更多选项。但另一方面,您还必须支持更多要求:并行请求,共享对象以及工作线程,分布式应用服务器等之间的状态。

如果您首先知道自己需要什么,那么您只能回答适合自己的问题。但实际上,为什么你还要编写另一个新框架呢?

答案 1 :(得分:1)

当依赖注入可行时,单身人士不赞成(我还没有找到需要单身人士的情况)。

您很可能控制控制器的实例化,因此您可以使用上面提到的$controller->setApplication($application),但如果有必要,您可以使用静态方法和变量(它们对正交性的影响远远小于申请比单身人士);即Controller::setApplication(),并通过实例方法访问静态变量。

例如:

// defining the Application within the controller -- more than likely in the bootstrap
$application = new Application();
Controller::setApplication($application);

// somewhere within the Controller class definition
public function setContentType($contentType)
{
    self::$application->setContentType($contentType);
}

我习惯于分离静态和实例属性和方法(必要时,仍然在类定义的顶部对属性进行分组)。我认为这比单身人士更不笨,因为课程仍然非常紧凑。

答案 2 :(得分:1)

重构怎么样?

当然,这不是您的选择之一,但您声明代码是一个很大程度上耦合的类。为什么不花时间和精力将它重构为更模块化,可测试的组件呢?

答案 3 :(得分:1)

据我了解,您的Application类应该是调度程序。如果是这样,我宁愿使用控制器构造函数来传递Application的实例,这样控制器就会知道谁在调用它。稍后,如果您希望拥有一个不同的Application实例,具体取决于是否从CLI中调用代码,您可以拥有一个ApplicationInterface,Application \ Http和Application \ Cli将实现它,并且一切都很容易维护。

你也可以实现一些工厂模式来获得一个很好的DI实现。例如,请在此处检查createThroughReflection方法:https://github.com/troelskn/bucket/blob/master/lib/bucket.inc.php

我希望这是有道理的。

此致 尼克

答案 4 :(得分:1)

您还可以使用ControllerFatory,您可以在其中提供您的应用程序或路由器/调度程序

你可以调用$ controllerFactory-> createController($ name);

您的应用程序根本不知道如何创建工厂的控制器。由于您可以将自己的ControllerFactory注入DI容器,因此您可以根据控制器管理所需的所有依赖项。

class ControllerFactory {
    public function __construct(EvenDispatcher $dispatcher,
                                Request $request,
                                ResponseFactory $responseFactory, 
                                ModelFactory $modelFactory, 
                                FormFactory $formFactory) {
        ...
    }

    public function createController($name = 'Default') {
        switch ($name) {
            case 'User':
              return new UserController($dispatcher, 
                                        $request, 
                                        $responseFactory->createResponse('Html'), 
                                        $modelFactory->createModel('User'),
                                        $formFactory->createForm('User'),...);

              break;
            case 'Ajax':
              return new AjaxController($dispatcher, 
                                        $request, 
                                        $responseFactory->createResponse('Json'), 
                                        $modelFactory->createModel('User'));
              break;
            default:
                 return new DefaultController($dispatcher, $request, $responseFactory->createResponse('Html'));
        }
    }

} 

因此,您只需在DI容器中添加此工厂并将其传递给您的应用程序。 每当您需要一个新的控制器时,您将其添加到工厂,如果需要新的依赖项,您可以通过DI容器将它们提供给工厂。

class App {
    public function __construct(Router $router,Request $request, ControllerFactory $cf, ... ) {
      ...
    }

    public function execute() {
        $controllerName = $this->router->getMatchedController();
        $actionName $this->router->getMatchedAction();

        $controller = $cf->createController($controllerName);

        if(is_callable($controller, $actionName)) {
            $response = $controller->$action(request);
            $response->send();     
        }
    }
}

这不是生产代码,我还没有测试过,但这就是你将控制器与应用程序分离的方式。请注意,虽然这里有一个错误的耦合,因为我的控制器返回一个响应,我在App中执行响应。但就像我说的那只是一个小例子。

将模型,表单和控制器的工厂传递给各自的父母通常是一个好主意,因为最终会在引导时加载所有对象Graph,这非常糟糕且占用内存。

我知道这个答案已经获得批准,但这是我对该主题的2美分

有一篇关于这个主题的好文章

http://miller.limethinking.co.uk/2011/07/07/dependency-injection-moving-from-basics-to-container/