PHP MVC:控制器中的依赖项太多?

1 php model-view-controller constructor design-patterns dependency-injection

我正在做一个个人 HMVC 项目:

  • 没有服务定位器,没有全局状态(比如staticglobal),没有单例。
  • 模型处理封装在服务中(服务 = 域对象 + 存储库 + 数据映射器)。
  • 所有控制器都扩展了一个抽象控制器。
  • 所有项目依赖都是通过Auryn依赖注入容器注入的。

所有需要的依赖项都被注入到抽象控制器的构造函数中。如果我想覆盖这个构造函数,那么我也必须在子控制器的构造函数中传递所有这些依赖项。

class UsersController extends AbstractController {

    private $authentication;

    public function __construct(
        Config $config
        , Request $request
        , Session $session
        , View $view
        , Response $response
        , Logger $logger
        , Authentication $authentication // Domain model service
    ) {
        parent::__construct(/* All dependencies except authentication service */);
        $this->authentication = $authentication;
    }

    // Id passed by routing.
    public function authenticateUser($id) {
        // Use the authentication service...
    }

}
Run Code Online (Sandbox Code Playgroud)

依赖项列表将进一步增长。这需要改变。于是我在想:

  • 将控制器与视图完全分开
    然后他们将共享服务层。视图将不再属于控制器,并且Response将成为视图的依赖项。
  • 使用setter注入在控制器
    RequestSessionLogger,等;

  • 仅在需要时才在控制器操作中注入依赖项
    喜欢Request, Session,Logger,等;
  • 使用装饰器模式。
    喜欢在动作调用后进行记录。
  • 实施一些工厂
  • 为了构造仅注入只在孩子控制器所需的依赖
    所以不进去AbstractController了。

我正在努力寻找一种优雅的方式来处理这项任务,我将不胜感激任何建议。谢谢你。

小智 5

我会回答我自己的问题。当我写它时,我已经很好地概述了许多有经验的开发人员关于 MVC 和 MVC 结构中的依赖注入的建议。

  • 构造函数注入是合适的选择。但在我看来,按照这一行,我最终会在构造函数中得到太多的依赖项/参数。因此赋予控制器太多的职责(读取请求的值、更改域对象的状态、记录操作、请求视图加载模板和呈现数据等)。
  • Setter 注入也是一个需要考虑的解决方案。但是,在我项目的开发过程中,我意识到这个解决方案确实不适合(至少)我的控制器-视图关系图。
  • 依赖项直接注入控制器动作也让我感到困难(但很开心),因为我已经将 url 值作为动作参数注入,并且我没有使用任何路由调度程序。
  • 实现工厂也是一个好主意,以便能够在每个控制器操作中拥有可供我使用的对象。工厂是一个很好用的工具,但只是从所需的运行时对象的前提出发,而不仅仅是减少构造函数中的依赖项的数量。
  • 装饰者模式也是一个不错的选择。但是,例如,如果您想在控制器操作中记录某些内容,那么这不是解决方案:您仍然必须将记录器作为依赖项(在构造函数、setter 或操作中)传递。
  • 我想过只在子控制器上注入所需的依赖项。但是相应控制器的多重职责的问题还是一样。

因此,无论我做了什么,这些解决方案似乎都不适合我的 HMVC 项目的结构。所以,我进一步挖掘,直到我意识到缺失的环节是什么。为此,我非常感谢汤姆巴特勒,他是以下伟大文章的创造者:


他的作品基于对 MVC 概念的深入、充分论证的分析。它们不仅很容易理解,而且还可以通过不言自明的例子来支持。一句话:对 MVC 和开发者社区的杰出贡献。

我将进一步写的内容只是用我自己的话来介绍他的原则,考虑以某种方式完成它们,提供更紧凑的视角,并展示我在我的应用程序中实施它们时所遵循的步骤项目。此处描述的有关该主题、想法和原则以及工作流程的所有功劳均归功于Tom Butler

那么,我的 HMVC 项目中缺少的链接是什么?它被命名为SPARATION OF CONCERNS

为简单起见,我将尝试通过仅提及一个控制器、一个控制器操作、一个视图、一个模型(域对象)和一个模板(文件)来解释这一点,并将它们引入User上下文。

Web 上最常描述的 MVC 概念 - 也由我研究过的一些流行框架实现 - 以让控制器控制视图和模型的原则为中心。为了在屏幕上显示一些东西,你必须告诉控制器——他进一步通知视图加载和渲染模板。如果这个显示过程也暗示使用一些模型数据,那么控制器也会操纵模型。

以经典的方式,控制器创建和动作调用过程包括两个步骤:

  • 创建控制器 - 将所有依赖项传递给它,包含视图;
  • 调用控制器动作。

编码:

$controller = new UserController(/* Controller dependencies */);

$controller->{action}(/* Action dependencies */);
Run Code Online (Sandbox Code Playgroud)

这意味着,控制器负责一切。所以,难怪为什么一个控制器必须注入这么多依赖项。

但是控制器是否应该参与或负责在屏幕上有效地显示任何类型的信息?不,这应该是视图的责任。为了实现这一点,让我们开始将视图与控制器分离——从不需要任何模型的前提出发。涉及的步骤是:

  • 定义output在视图中的屏幕上显示信息的方法。
  • 创建控制器 - 将所有依赖项传递给它,除了视图及其相关依赖项(响应、模板对象等)。
  • 创建视图 - 将相应的依赖项(响应、模板对象等)传递给它。
  • 调用控制器动作。
  • 调用output视图的方法:

和代码:

class UserView {
    //....
    // Display information on screen.
    public function output () {
        return $this
                    ->load('<template-name>')
                    ->render(array(<data-to-display>))
        ;
    }
    //....
}

$controller = new UserController(/* (less) controller dependencies */);
$view = new UserView(/* View dependencies */);

$controller->{action}(/* Action dependencies */);
echo $view->output();
Run Code Online (Sandbox Code Playgroud)

通过完成上面的五个步骤,我们设法将控制器与视图完全分离。

但是,有一个方面,我们之前假设过:我们没有使用任何模型。那么控制器在这个星座中的作用是什么?答案是:没有。控制器应该仅作为某个存储位置(数据库、文件系统等)和视图之间的中间人存在。否则,例如仅以某种格式在屏幕上输出一些信息,output视图的方法就完全足够了。

如果模特被带到现场,事情就会改变。但是应该在哪里注射呢?在控制器中还是在视图中?同时。它们共享相同的模型实例。在这一刻,控制器凭借自己的权利获得了中间人的角色——介于存储和视图之间。一种理论形式是:

$model = new UserModel;
$controller = new UserController($model, /* Other controller dependencies */);
$view = new UserView($model, /* Other view dependencies */);

$controller->{action}(/* Action dependencies */);
echo $view->output();
Run Code Online (Sandbox Code Playgroud)

这样做,控制器可以更改模型的状态并确保它保存在存储系统中。视图读取并显示相同的模型实例及其状态。控制器通过模型将显示逻辑信息传递给视图。问题是,这些信息不属于模型应该专有的业务逻辑。他们只是显示逻辑参与者。

为了避免将显示逻辑责任交给模型,我们必须在图中引入一个新组件:视图模型。控制器和视图将共享一个视图模型实例,而不是共享模型对象。只有这个将接收模型作为依赖项。一个实现:

$model = new UserModel;
$viewModel = new UserViewModel($model, /* Other view-model dependencies */);
$controller = new UserController($viewModel /* Other controller dependencies */);
$view = new UserView($viewModel, /* Other view dependencies */);

$controller->{action}(/* Action dependencies */);
echo $view->output();
Run Code Online (Sandbox Code Playgroud)

工作流程可以这样描述:

  • 请求值由浏览器(“用户”)发送到控制器。
  • 控制器将它们作为属性(数据成员)存储在视图模型实例中,从而改变视图模型的显示逻辑状态。
  • 在它的output方法中,视图从视图模型中读取值并请求模型根据它们查询存储。
  • 模型运行相应的查询并将结果传递回视图。
  • 视图读取并将它们传递给相应的模板。
  • 模板渲染后,结果显示在屏幕上。

视图模型不属于域模型,所有域对象都驻留在域模型中,真正的业务逻辑发生在域模型中。它也不属于操作域对象、存储库和数据映射器的服务层。它属于应用程序模型,例如应用程序逻辑发生的地方。视图模型获得从控制器获取显示逻辑状态并将其传递给控制器​​的唯一责任。

可以看出,只有视图模型“接触”了模型。控制器和视图不仅彼此完全解耦,而且与模型完全解耦。这种方法最重要的方面是,所涉及的所有组件中的每一个都只获得它应该获得的责任。

利用这种组件分离和依赖注入容器,控制器依赖过多的问题就消失了。并且可以以一种非常灵活的方式应用我的问题中提供的所有选项的组合。无需记住其中一个组件(模型、视图或控制器)承担了太多责任。