Lazy loading services using Zend Service Manager

Any more complex application includes a big dependency injection tree of services, some of which can have a more complicated creation logic. If the service is injected as a dependency, but not necessarily used at every execution, you may want to lazily initialize that service until it is really needed.

In those situations, you may be tempted to inject the entire Dependency Injection Container instead, and lazy-load that resource-hungry service. I find that to be an anti-pattern, and I explained my views in a blog post written some time ago.

To come up with a better solution, ask yourself a tricky question: how would you solve this puzzle without the help of a DI container, in a situation where you would have to manually assemble application services? Such a challenge evokes creativity, forces us to approach a problem from a different angle, and eventually leads us to think about design patterns.

Proxy design pattern makes it possible to avoid coupling to the DI container so that you keep dependencies explicit, while achieving the same goal of lazy-loading an object that is expensive to instantiate. Here is an example for a proxy implemented using an anonymous class:

$notifier = new class implements NotiferInterface {
    private $realNotifier;

    public function notify(RecipientsCollection $recipients, Notification $notification) : void
    {
        $this->getRealNotifier()->notify($recipients, $notification);
    }

    private function getRealNotifier() : NotiferInterface
    {
        if (null === $this->realNotifier) {
            $this->realNotifier = new MultiChannelNotifier(
                'email' => new NativeMailer(),
                'sms' => new TwilioSMS('token', new HttpClient()),
                'push' => Pushover('token', new HttpClient()),
            );
        }

        return $this->realNotifier;
    }
};

You can inject this efficient object in any one that depends on NotiferInterface, while complex logic for creating actual (real) notifier will be executed only when a notification is sent:

final class ArticleService
{
    public function comment(string $articleId, array $commentPayload)
    {
        $article = $this->articleRepo->get($articleId);
        $comment = Comment::fromInput($commentPayload);

        $article->add($comment);

        $this->articleRepo->save($article);

        //service is instantiated only here!
        $this->notifier->notify(
            new RecipientsCollection([
                $article->getAuthor(),
            ]), 
            new NewCommentNotification($comment)
        );
    }
}

Some DI container solutions, such as Zend Service Manager, go a step further with features that facilitate the use of different creational patterns. The one that is relevant to our story is LazyServiceFactory, which completely eliminates the effort and hides the complexity of proxying application services. Internally, it uses ProxyManager - library that provides abstraction for generating proxy classes, so you will need to install that package before using this feature.

NotifierFactory.php

final class NotiferFactory 
{
    public function __invoke(ContainerInterface $container) : NotiferInterface
    {
        return new MultiChannelNotifier(
           'email' => new NativeMailer(),
           'sms' => new TwilioSMS('token', new HttpClient()),
           'push' => Pushover('token', new HttpClient()),
       );
    }
};

ArticleServiceFactory.php

final class ArticleServiceFactory 
{
    public function __invoke(ContainerInterface $container) : ArticleService
    {
        return new ArticleService(
           $container->get(ArticleRepositoryInterface::class),
           $container->get(NotifierInterface::class),
       );
    }
};

services.php

use MyApp\NotifierFactory;
use MyApp\NotifierInterface;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\Proxy\LazyServiceFactory;
use Zend\ServiceManager\ServiceManager;

$serviceManager = new ServiceManager([
    'factories' => [
        ArticleService::class => ArticleServiceFactory::class,
        NotifierInterface::class => NotifierFactory::class,
    ],
    'delegators' => [
        NotifierInterface::class => [
            LazyServiceFactory::class,
        ],
    ],
    'lazy_services' => [
         'class_map' => [
             NotifierInterface::class => NotifierInterface::class,
         ],
    ],
]);

return $serviceManager;

With just a few lines of configuration code, LazyServiceFactory does all the heavy lifting, and an efficient Notifier object gets returned behind the scenes whenever you require it from a DI Container.

Performance is a crucial feature of the applications we develop today, every millisecond and each memory byte counts, so optimizing the assembly part of our application can save valuable resources. Thereby, do not stuck with the easiest solution, think twice every time you are tempted to leak DI Container into the code. Breaking the Dependency Rule is not a valid compromise, because for every problem there is a proper solution. Tools such as Zend Service Manager facilitate the work needed even more.


zf factory proxy service manager