PHP MVC:控制器中的依赖项过多? [英] PHP MVC: Too many dependencies in controller?

查看:55
本文介绍了PHP MVC:控制器中的依赖项过多?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在从事一个个人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...
    }

}

依赖项列表将进一步增长.这需要改变.所以我在想:

  • 将控制器与视图完全分开.
    然后,他们将共享服务层.这些视图将不再属于控制器,并且Response将是视图的依赖项.
  • 在控制器中使用 setter注入
    ,类似于RequestSessionLogger等;
  • 在控制器动作中注入依赖项
    仅在需要时.
    类似于RequestSessionLogger等;
  • 使用装饰器模式.
    类似于在调用操作后进行记录.
  • 实施一些工厂.
  • 构造方法只将所需的依赖项仅注入子控制器上.
    所以不再使用AbstractController了.

我正在尝试找到一种优雅的方式来处理此任务,我将不胜感激任何建议.谢谢.

解决方案

我将回答我自己的问题.当我写它的时候,我已经对许多经验丰富的开发人员在MVC和MVC结构中的依赖注入方面的建议有了很好的了解.

  • 构造函数注入是正确的选择.但这似乎 我认为,按照这条线,我最终会遇到太多 构造函数中的依赖项/参数.因此给控制器 职责过多(读取要求的值,更改 域对象的状态,日志记录操作,请求视图 加载模板并渲染数据等).
  • Setter注入也是要考虑的解决方案.但是,在我的项目开发期间,我意识到 该解决方案确实(至少)不适合(至少) 我的控制器与视图的关系.
  • 依赖项直接注入到控制器中 行动,也要记住我的经历(虽然很开心) 我已经将url值注入为操作参数,并且 我没有使用任何路由调度程序.
  • 实施工厂也是一个好主意,以便能够 在每个控制器动作中都有我可以使用的对象. 工厂是一个很好的使用工具,但前提是 所需的运行时对象,而不仅仅是减少 构造函数中的依赖项.
  • 装饰图案也是不错的选择.但是,例如,如果您想在控制器操作中记录某些内容,则 这不是解决方案:您仍然必须通过记录器作为依赖项 (在构造函数,setter或action中).
  • 我曾考虑过仅在以下方面注入所需的依赖关系: 子控制器.但是,那么多重的问题 相应控制者的职责保持不变.

所以,无论我做什么,这些解决方案似乎都不完全适合我的HMVC项目的结构.因此,我进一步挖掘,直到我意识到缺少的链接是什么.为此,我要感谢以下伟大文章的作者 Tom Butler :


他的作品基于对MVC概念的深入论证.它们不仅易于遵循,而且通过不言自明的示例得到了支持.一句话:对MVC和开发人员社区的杰出贡献.

我将进一步写的内容只是用我自己的话介绍他的原则,并牢记以某种方式完成这些原则,以更紧凑的角度介绍这些原则,并展示我在实施时所遵循的步骤他们在我的项目中.此处所述的所有主题,思想,原则和工作流程都归功于 Tom Butler .

那么,我的HMVC项目中缺少什么链接?它的名称为担忧的分离.

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

在Web上最多描述的MVC概念-并由我研究的一些流行框架实现-围绕为控制器提供对视图和模型的控制的原理.为了在屏幕上显示某些内容,您必须将其告知控制器-他进一步通知视图以加载和呈现模板.如果此显示过程也暗示使用某些模型数据,则控制器也将操纵模型.

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

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

代码:

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

$controller->{action}(/* Action dependencies */);

这暗示着,控制器负责一切.因此,难怪为什么必须向控制器注入如此多的依赖项.

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

  • 定义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();

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

但是,有一个方面,我们较早地进行了假设:我们没有使用任何模型.那么,控制器在这个星座中的作用是什么?答案是:没有.控制器应仅作为中间存储存在于某个存储位置(数据库,文件系统等)和视图之间.否则,例如仅要在屏幕上以某种格式输出某些信息,视图的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();

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

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

$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();

工作流程可以这样描述:

  • 请求值由浏览器(用户")发送到 控制器.
  • 控制器将它们作为属性存储在视图模型实例中 (数据成员),因此更改了 视图模型.
  • 在其output方法中,视图从以下位置读取值: 查看模型并请求模型查询其上的存储 基础.
  • 模型运行相应的查询,并将结果传递回 视图.
  • 该视图读取并将它们传递给相应的模板.
  • 渲染模板后,结果显示在屏幕上.

视图模型不属于域模型,所有域对象都驻留在该模型中,并且实际的业务逻辑就会发生.它也不属于服务层,后者处理域对象,存储库和数据映射器.它属于应用程序模型,例如属于应用程序逻辑发生的地方.视图模型承担从控制器获取显示逻辑状态并将其传导到控制器的全部责任.

可以看出,只有视图模型才接触"模型.控制器和视图不仅完全彼此分离,而且还与模型完全分离.这种方法最重要的方面是,所涉及的所有组件中的每一个都只承担应该承担的责任.

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

I'm working on a personal HMVC project:

  • No service locators, no global state (like static or global), no singletons.
  • The model handling is encapsulated in services (service = domain objects + repositories + data mappers).
  • All controllers extend an abstract controller.
  • All project dependencies are injected through Auryn dependency injection container.

All needed dependencies are injected in the constructor of the abstract controller. If I want to override this constructor, then I have to pass all these dependencies in the child controller's constructor too.

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...
    }

}

The dependencies list would further grow. This needs to change. So I was thinking about:

  • Totally separate controllers from views.
    They would then share the service layer. The views wouldn't belong to controllers anymore and the Response would be a dependency of the views.
  • Use setter injection in controllers
    Like for Request, Session, Logger, etc;
  • Inject dependencies in the controller actions
    Only when needed.
    Like for Request, Session, Logger, etc;
  • Use decorator pattern.
    Like for logging after an action call.
  • Implement some factories.
  • To constructor inject only the needed dependencies only on child controllers.
    So not in AbstractController anymore.

I'm trying to find an elegant way to deal with this task and I'll appreciate any suggestions. Thank you.

解决方案

I'll answer to my own question. When I wrote it I already had a good overview of what many experienced developers recommend regarding dependency injection in MVC and MVC structure.

  • The constructor injection is the proper option. But it seemed to me that, following this line, I'll end up with too many dependencies/arguments in constructors. Therefore giving controllers too many responsibilities (reading the requested values, changing the state of the domain objects, logging operations, requesting the view to load the templates and to render the data, etc).
  • Setter injection was also a solution to take in consideration. But, in the course of the developing time on my project, I realised that this solution really didn't fit (at least) into the picture of my controller-view relationships.
  • The injection of the dependencies directly into the controller actions caused me hard-time (but great time) too, having in mind that I already had the url values injected as action arguments and that I didn't used any routing dispatchers.
  • Implementing factories was also a great idea, in order to be able to have objects at my disposal within each controller action. Factories are a great tool to use, but only going from the premise of needed run-time objects, not of just reducing the number of dependencies in constructors.
  • Decorator pattern is also a good alternative. But if, for example, you want to log something within a controller action, then this is not a solution: you still have to pass a logger as dependency (in constructor, setter or action).
  • I had a thought about injecting the needed dependencies only on child controllers. But then the problem of multiple responsibilities of the corresponding controllers remains the same.

So, none of this solutions seemed to entirely fit into the structure of my HMVC project, whatever I did. So, i dug further, until I realised what the missing link was. For this I give my whole appreciation to Tom Butler, the creator of the following great articles:


His works are based on an in-depth, well argumented analyse of the MVC concepts. They are not only very easy to follow, but also sustained by self-explanatory examples. In a word: a wonderful contribution to the MVC and developer community.

What I'll write further is meant to be just a presentation of his principles with my own words, having in mind to somehow complete them, to provide a compacter perspective of them and to show the steps followed by me when I implemented them in my project. All credit on the subject, ideas and principles and workflows depicted here goes to Tom Butler.

So, what was the missing link in my HMVC project? It's named SEPARATION OF CONCERNS.

For simplicity I'll try to explain this by referring myself to only one controller, one controller action, one view, one model (domain object) and one template (file), introducing them into a User context.

The MVC concept most described on web - and also implemented by some popular frameworks that I studied - is centered around the principle of giving the controller the control over the view and the model. In order to display something on screen you'll have to tell that to the controller - he further notifies the view to load and render the template. If this display process implies the use of some model data too, then the controller manipulates the model too.

In a classical way, the controller creation and action calling process involve two steps:

  • Create the controller - passing it all dependencies, view inclusive;
  • Call the controller action.

The code:

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

$controller->{action}(/* Action dependencies */);

This implies, that the controller is responsible for, well, everything. So, no wonder why a controller must be injected with so many dependencies.

But should the controller be involved in, or responsible for effectively displaying any kind of information on the screen? No. This should be the responsibility of the view. In order to achieve this let's begin separating the view from the controller - going from the premise of not needing any model yet. The involved steps would be:

  • Define an output method for displaying information on screen in the view.
  • Create the controller - passing it all dependencies, excepting the view and its related dependencies (response, template object, etc).
  • Create the view - passing it the corresponding dependencies (response, template object, etc).
  • Call the controller action.
  • Call the output method of the view:

And the code:

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();

By accomplishing the five upper steps we managed to completely decouple the controller from the view.

But, there is an aspect, that we hypothesised earlier: we did not made use of any model. So what's the role of the controller in this constellation then? The answer is: none. The controller should exist only as a middlemann between some storage place (database, file system, etc) and the view. Otherwise, e.g. only to output some information in a certain format on screen, is the output method of the view fully sufficient.

Things change if a model is beeing brought on the scene. But where should it be injected? In the controller or in the view? In both. They share the same model instance. In this moment the controller gains the role of the middleman - between storage and view - in his own right. A theoretical form would be:

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

$controller->{action}(/* Action dependencies */);
echo $view->output();

Doing so, the controller can change the state of the model and ensure that it's saved in the storage system. The same model instance, respective its state, is read by the view and displayed. The controller passes display logic informations to the view through the model. The problem is, that these informations don't belong to the business logic, which a model is supposed to exclusively have. They are only display logic participants.

In order to avoid giving display logic responsibilities to the model, we have to introduce a new component in the picture: the view-model. Instead of sharing a model object, the controller and the view will share a view-model instance. Only this one will receive the model as dependency. An implementation:

$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();

And the workflow can be described like this:

  • The request values are sent by the browser ("the user") to the controller.
  • The controller stores them in the view-model instance as properties (data members), therefore changing the display logic state of the view-model.
  • Within its output method, the view reads the values from the view-model and requests the model to query the storage on their basis.
  • The model runs the corresponding query and passes the results back to the view.
  • The view reads and passes them to the corresponding template.
  • After template rendering, the results are displayed on the screen.

The view-model does not belong to the domain model, where all domain objects reside and the real business logic takes place. It does also not belong to the service layer, which manipulates the domain objects, repositories and data mappers. It belongs to the application model, e.g to the place where the application logic takes place. The view-model obtains the sole responsibility of gaining display logic state from the controller and conducting it to the controller.

As can be seen, only the view-model "touches" the model. Both, the controller and the view were not only completely decoupled from each other, but also from the model. The most important aspect of this approach is, that each of all the components involved gains only the responsibilities, that it is supposed to gain.

By making use of this kind of component separation and of a dependency injection container, the problem of too many dependencies in controllers vanishes. And one can apply a combination of all options presented in my question in a really flexible manner. Without having in mind that one of the components (model, view or controller) gains too many responsibilities.

这篇关于PHP MVC:控制器中的依赖项过多?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆