Best approach to add infrastructure class to other classes

Hi there
In my opinion some class like db,cache,logger is infrastructure class,means almost all other classes need those.
what is best approach to prevent add those class to __construct other classes.
I’m searching something like *awareInterface and/or trait(interface injection) to work in expressive.

This is not really an “Expressive” problem, you should solve this in your container layer. (zend-servicemanager offers a few ways to do this for example)

Could you please address some example?

I’m not sure what you mean by most clases. It depends on how you setup your app. I only inject the logger where I really need it. Mostly I want to know about exceptions so I attach the logger to the listener.

I use doctrine and I don’t inject the entire database / entity manager. All I inject is the repository classes which are needed. Most of the time it’s only 1, and occasionally 2 repositories.

Caching is magically done in doctrine, twig (template renderer) and I have a caching middleware to cache static pages.

Using SomeServiceAwareInterface or traits are not recommended. The recommended way is using factories and inject those into the constructor. Yes, it is a bit more work, but after writing a few you get pretty fast at it. Besides that, zend-servicemanager has some console tools available to generate those factories. Once you have the factories in place it gets a lot easier to write tests as well.

2 Likes

It’s actually good question :slight_smile: I was stuck with the same question and tried in production-apps some of solutions as you mentioned awareInterface, traits, facades, different techniques of dependency injections, events etc. etc.

At the end I realize one thing was wrong in that question and that is " …prevent add those class to __construct other classes."

Shortly, you don’t want to prevent that.
If your class have dependency than show it!

It’s better to show all “dirty staff” sooner in constructor. Otherwise you will hide it and make a potential problem later. If your class needs to much other classes well… that is bad and you should organize code in different way but not hiding dependency.

Try to organize your code in separated small packages instead
and if all packages/classes needs logger than inject it in constructor… and thanks me later :smiley:

2 Likes

As several others have noted, pushing such infrastructure concerns into all of your other classes and/or middleware likely may not be necessary. I find this particularly true of middleware, where you can “decorate” your pipeline to add these features via standalone middleware. Need to log the request and response from a given middleware? Add logging middleware to its pipeline. Need to add caching for a given middleware? Add caching middleware to its pipeline. Middleware pipelines essentially provide an intercepting filter implementation, which allows you to separate your core logic from these other concerns.

Database access is another thing entirely, of course, as that will be highly dependent on the endpoint; the same is true of things like templating. For these, try and standardize on a handful of constructor patterns, and then write a small number of factories that you can then apply to multiple middleware. zend-servicemanager allows you to do this, as it passes a second argument to each factory, the service name:

class MiddlewareWithTemplateAndDbFactory
{
    public function __invoke(ContainerInterface $container, string $serviceName) : MiddlewareInterface
    {
        return new $serviceName(
            $container->get(TemplateRendererInterface::class),
            $container->get(DbAdapter::class)
        );
    }
}

You can then apply this to multiple services:

'factories' => [
    BlogPostMiddleware::class => MiddlewareWithTemplateAndDbFactory::class,
    ListBlogPostsMiddleware::class => MiddlewareWithTemplateAndDbFactory::class,
]

While you can certainly go the route of *AwareInterface implementation, doing so requires that you add delegator factories for each service, and this adds a new vector for bugs: what if you forget to wire the necessary delegator factories for a given service? This is difficult to debug, and if somebody on your team is unaware of how these work, they may wonder why the factory doesn’t inject those dependencies in the first place, and end up writing an entirely new factory, needlessly.

The takeaways I have for you are:

  • Use the existing infrastructure when you can (e.g., the caching and logging features of Doctrine).
  • Use middleware as intercepting filters so you can delegate these infrastructure concerns to standalone, dedicated, re-usable middleware.
  • Standardize on a handful of constructor patterns for common dependencies, and share factories.
2 Likes