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.

Controllers are not Services

Controllers (in webbish MVC) and Actions (in ADR) are not Services. That is, their instances are not shared throughout the system. They are not shared objects used in many different places. Each is created and used only once, in one place, usually as the result of routing logic.

As such, Controllers and Actions should not be defined as "Services" in Dependency Injection containers. They should not be in a Service Locator, either.

Instead, try to use a Factory to create the one Controller or Action that you need for the interaction.

UPDATE (7 Dec 2019): Based on the Reddit discussion below, I have reason to revise my opinion. Look for a followup post in the next couple of days.


See the Reddit conversation on this post here.

Teams do not accomplish anything of genuine intellectual value

[T]eams are for normies, for neurotypicals, for trash people who can’t retain multiple levels of variable dereferencing in their heads while coding.

Teams do not accomplish, and have never accomplished, anything of genuine intellectual value. The history of scientific progress is a history of individuals.

Yes, you need a "team" to actually assemble the atomic bomb or the Intel Itanium or a commercial software product. You don’t need a team to conceive it and do the mental heavy lifting.

The effective IQ of a team is the same as the lowest IQ in the team; the productivity of the team is a minor percentage of the productivity you could get from its smartest member working alone.

Every once in a while you will see one brilliant person be inspired by another brilliant person in the near vicinity. This happens once for every hundred million times a "team" crushes the abilities of its members.

Science, even computer science, is not football.

I find myself sympathetic to this outlook. Via Weekly Roundup: The Passion Of Saint iGNUcius Edition.

Speaking at Bulgaria PHP 2019

I mentioned a couple of days ago that, as favors to friends, I am temporarily relaxing my self-imposed restriction on speaking at conferences I have to fly to. The first was EEConf 2019; the second is Bulgaria PHP.

As the opening talk of day 2, I'll be giving a brand-new presentation on "Rethinking What You Think You Know." It's nominally about the road to Action Domain Responder, but I expect to include more than just that. See you there!

Don’t Believe All Women, Because Some Are Terrible

"Maybe we should treat people like people:"

[I]if we are required to believe every woman who makes an accusation, then every allegation, simply by the act of making it, becomes “credible.” This leaves little room for raising questions about honesty and due process, and plenty of space for ideological distortion.

And since ideology abhors nuance, simple narratives come to dominate the discussion when an allegation is made (in Kavanaugh’s case: beer-drinking private school boy = bad; beer-drinking private school girl = good). And anything that contradicts the narrative is explained away or ignored (such as, being drunk is damning evidence if you’re the accused man, but exculpatory if you’re the woman who made the allegation).

Via https://www.commentarymagazine.com/politics-ideas/dont-believe-all-women-because-some-are-terrible/.

Speaking at EEConf 2019

I have been lax in pointing out that I will be speaking at EEConf 2019 in October. Here's a preview of the topic, and some other background:

I have for some years now sworn off speaking at conferences that I have to fly to. I hate air travel, not only because of the experience itself but because I often get sick, and once brought illness home to my family. But in this case (and in one other to be mentioned later!) I am making an allowance as a favor for a friend.

See you at EEConf!

Knitting Has Always Been Political

This is a couple of months old, but it echoes the totalitarian SJW attitudes in software as well:

Ravelry, a popular website for knitters and crocheters, took a political stand when it announced that it was banning content that supports President Trump, in what it said was a resolution against white supremacy.

“We cannot provide a space that is inclusive of all and also allow support for open white supremacy,” the site said in a statement explaining the decision. “Support of the Trump administration is undeniably support for white supremacy.”

The policy applies to content on the site, including knitting patterns and forum posts, but not to people, according to Ravelry, which said it still welcomed Republicans and those with conservative political views. “You can still participate if you do in fact support the administration, you just can’t talk about it here,” the statement said, adding that “hate groups and intolerance are different from other types of political positions.”

To the SJW, the totality of everything is political. There is no non-political space. All your hobbies and interests must be held hostage to social justice.

Via NYTimes with Twitter commentary here.

Farewell, Wendy

Wendy Lou Jones, aged around 17, passed away this morning. She was good when she was not being naughty. Clever, resilient, thiefy little dog. :-)

puppy-wendy

winter-wendy

mature-wendy

Moving Away From Google

In an effort to extract Google's tentacles from my life, and to give them as little feedstock as possible, I am no longer using my Gmail account as a primary contact address. Contact me at my pmjones.io address instead. Likewise, my paul-m-jones.com address is no longer being hosted by Google.

It was surprisingly time-consuming to find a reasonable alternative host that would support some other functionality (more on that later). After some preliminary attempts, I settled on Namecheap. Believe it or not, I am using a shared hosting plan, which supports another effort I am trying out (more on that in another post).