Controllers are Services

tl;dr: Contra my previous opinion, controllers are Service objects; my understanding of a "Service", regarding DI/SL Containers, was incorrect. However, I do continue to opine that it is better use a Factory to get new Controller instances, rather than getting them from the Container directly in a non-Factory object. All that and more in this followup post.


I

My previous post generated a great discussion on Reddit between myself and /u/ahundiak, which I suggest you read in its entirety. To summarize:

  • I thought of Services in a Container as objects that are retained for reuse throughout the application; e.g. a PDO instance, a logger, etc.

  • ahundiak disagreed, and pointed to the Symfony explanation of a Service as "objects that do something", even when not shared throughout the system.

  • I found that explanation unsatisfying and not well-defined.

  • However, ahundiak also said "Symfony only creates service definitions for objects which are actually injected somewhere." That piqued my interest, because it got closer to the heart what I was trying to express.

From there we ventured a bit further afield.

II

Based on that conversation, I did a couple hours' of research, using variations on "what is a service object" (and you get some unexpected search results in a couple of cases). In the end, though, I found myself at the Misko Hevery article To new or not to new.

I have taken Misko's advice before, particularly from his article on How to Think About the “new” Operator with Respect to Unit Testing; I believe I refer to it in MLAPHP. In that article, his final summary point is that your classes should either have application logic in them, or have new operators, but not both.

The idea is that object construction is a concern to be separated from object use. The end result is that you put all object construction into Factories, and anything that needs to create an object uses the Factory instead of the new operator.

In his later article on "To new or not", Misko lays out definitions around two distinct groups of objects within any application. Lightly edited for comprehension and emphasis, he says:

  • "Injectables" are objects which you will ask for in the constructors and expect the DI framework to supply. ... All external services are Injectables. ... Sometimes I refer to Injectables as Service Objects, but that term is overloaded.

  • There are a lot of objects out there which DI framework will never be able to supply. Let's call these “Newables” since you will be forced to call new on them manually. ... Newables are objects which are at the end of your application object graph. ... Sometimes I refer to Newables as Value Object, but again, the term is overloaded.

  • It is OK for Newable to know about Injectable. What is not OK is for the Newable to have a field reference to Injectable.

Now that is something I can wrap my head around. To a first approximation, Newables/Values are leafs on the object graph; everything else on the path to a Newable/Value is an Injectable/Service.

I think that very nicely backs up ahundiak's reference to Symfony's explanation about Service objects, with some additional and welcome rigor.

Misko goes on to define some rules-of-thumb around Injectables/Services and Newables/Values. Paraphrasing and summarizing:

  • An Injectable can ask for primitives (int, float, string, etc.) and other Injectables in its constructor, but never a Newable.

  • Conversely, a Newable can ask for primitives and other Newables in its constructor, but never an Injectable.

III

Based on Misko's definition (which is obviously better than my prior one) Controllers are Services that a DI container can provide. Mea culpa.

However, I continue to stand by a corollary opinion I expressed in the previous article: that you should not retain a Controller instance for reuse inside the Container. Instead, you should use a Factory to create the Controller instance. Further (and I did not say this explicitly) you should typehint on that Factory instead of the Container itself.

Now, it is well within the proper scope of a Container to be used as a Factory. A Container plays two roles: that of Factory (to create new object instances) and of Registry (to retain already-created instances for shared use). So in a Dispatcher, for example, you could do this to run a Controller based on Route information:

class Dispatcher
{
    protected $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function dispatch(Route $route)
    {
        $class = 'App\Http\Controller\\' . $route->getController();
        $object = $this->container->new($class);
        $method = $route->getAction();
        $params = $route->getParams();
        return $object->$method(...$params);
    }
}

That's not bad -- by calling new() on the Container, you can tell the Container is being used as a Factory, not a Registry. (Some Containers will allow you to configure certain types always to be new instances, so that calling get() will always return a new instance, but I am not a fan of that idiom.)

IV

However, if you you consider typehints as communication, then typehinting on Container communicates "the Dispatcher will need to create or locate anything the Container can provide." It's using the Container as a Service Locator, and in general we ought to prefer Dependency Injection.

As an alternative, we might typehint on a ControllerFactory instead. That kind of typehint communicates a narrower requirement: "the Dispatcher will need to create new Controller instances."

Putting together a ControllerFactory is easy enough ...

class ControllerFactory
{
    protected $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function new(string $suffix) : Controller
    {
        $class = 'App\Http\Controller\\' . $suffix;
        return $this->container->new($class);
    }
}

... and then the Dispatcher can depend on that, instead of the Container:

class Dispatcher
{
    protected $controllerFactory;

    public function __construct(ControllerFactory $controllerFactory)
    {
        $this->controllerFactory = $controllerFactory;
    }

    public function dispatch(Route $route)
    {
        $object = $this->controllerFactory->new($route->getController());
        $method = $route->getAction();
        $params = $route->getParams();
        return $object->$method(...$params);
    }
}

Of course, the ControllerFactory itself now depends the Container -- the dependency on the Container just moved, it didn't disappear. Even so, this arrangement is less objectionable for a couple of reasons:

  • The "creation of objects" concern remains separated, per Hevery's guideline "to have either classes with logic OR classes with new operators."

  • The Controller instances being created really are likely to need a wide range of many different object injections, including shared objects from the Container. The typehint of Container is an accurate communication here.

V

As often happens, the answer leads to more questions. In this case, I begin to wonder about the kinds of objects that belong in a Container, per Misko's definitions. For example, is an HTTP Request object something that ought to be created and retained within a Container? I can think of no Request implementations that depend on Injectables; it seems more like a Newable, per Misko, in that it exists as a leaf on the object graph.

Well, I will leave that for another day. Many thanks to ahundiak for correcting me, or least leading me to correction, on this topic.


See the Reddit conversation on this post here.

Are you stuck with a legacy PHP application? You should buy my book because it gives you a step-by-step guide to improving you codebase, all while keeping it running the whole time.