MVC and dependency injection, forced to use single

2019-05-15 16:10发布

问题:

I'm working on building a PHP framework that behaves according to MVC principles and utilizes dependency injection. I think I have the front-controller part down; there is a working router that instantiates a controller instance and calls the appropriate action based on the requested URI.

Next up is dependency injection. I want to implement a Container that resolves dependencies using reflection. In doing so, I think I'm running into a problem with my controllers.

There are a number of what I call "system dependencies" that need to be available to derived controller classes. I haven't actually created all these dependencies yet, but it seems sensible that controllers have access to services like an InputProvider (to encapsulate get/post params or command line arguments), and maybe an Output dependency. Ideally, I'd use the framework's Container to inject these dependencies into the constructor of the controller - but this is where I run into problems.

If I use constructor injection for system dependencies in the controller, then I'm forcing derived controllers to manage the base controller's dependencies if they implement a constructor of themselves. That doesn't seem to be the most user-friendly. The other option is to use setter injection for system dependencies, but then derived controllers won't have access to these system dependencies if they should have need of them in their constructor.

The only solution I see that offers the best of both worlds is to make my controllers singletons. They'd have a private constructor so I can safely use setter injection without worrying about the constructors of derived classes. Instead, there would be an overridable initialize() method (assuming I get method injection working somehow), that basically fulfills the role of constructor (as in, an initializer for the derived controller). This way, constructor injection is replaced by method injection in the initialize() method, where all system dependencies would be available, without requiring the derived controller to manage them.

But then, a quick google search seems to unanimously say that singleton controllers are bad practice. I'm very unsure on how to proceed. I am probably overengineering this, but apart from wanting my application to be future-proof and maintainable, I also see it as a little exercise in applying best practices, so I'd like to do things "properly".
I think the best practice in this case would be to pass the responsibility of managing the required system dependencies to the derived controller. A dependency should probably only be instantiated if the derived controller actually has need of it. Why inject an InputProvider in the base controller if it is possible that a derived controller is never even going to use it? But at the same time, I keep coming back to user friendliness, and how nice it is to simply always have a $this->input member available, such as in a framework like CodeIgniter.

I highly appreciate any and all contributions to my dillemas. I also apologise for the wall of text, but I couldn't think of any code examples that would make the job of explaining any easier, since it's all so abstract to me right now!

Sincerely,
A severely torn individual

回答1:

Several solutions are possible:

  • forbid controllers to use __construct(): make it private public final, and make controllers override something like init() and call it from the constructor. Then the constructor would inject all the dependencies (reflection? other stuff?), so that they are all ready in init().

  • you can use an existing DI library like PHP-DI (disclaimer: I work on that) that will allow you to define dependencies but have them available in the constructor (magically, yes).

Something like that:

<?php
use DI\Annotations\Inject;

class FooController {
    /**
     * @Inject
     * @var Bar
     */
    private $bar;

    public function __construct() {
        // The dependency is already injected
        $this->bar->sayHello();
    }

    public function setBar(Bar $bar) {
        return $this->bar = $bar;
    }
}

For example that is how I work with Zend Framework 1. I can't use the constructors, so I inject into properties. Here is the ZF1 integration project.