Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Providing Dependency Injection in an own reusable library

Background

I am writing some reusable library, which contains few classes. One of them needs to have a dependency, because of some more complex logic and I want to delegate the responsibility of that class somewhere else (another class).

Goal

I don't want to create a bundle e.g. Symfony Bundle which could handle my dependency injection and provide a simple way to integrate it with a client code. My goal is to provide reusable and framework independent solution.

Additional information

I am using composer and I've read about DI containers like php-di. There is a demo application example by php-di, but it doesn't fit into my requirements.

Example

Fragment of library code

<?php

class W3CAnalyzer implements WebStandardAnalyzer
{
   private $httpClient;

   public function __construct(HttpClient $httpClient)
   {
      $this->httpClient = $httpClient;
   }

   public function analyze(string $url): WC3AnalysisMetaData
   {
      $siteContent = $this->httpClient->getContent($url);

      //further logic there
   }
}

As you can see W3CAnalyzer contains the dependency HttpClient class. I will somehow configure a DI container to register and resolve that dependency.

Sample client code

<?php

class WebStandardController
{
    private $webStandardAnalyzer;

    public function __construct(WebStandardAnalyzer $webStandardAnalyzer)
    {
       $this->webStandardAnalyzer = $webStandardAnalyzer;
    }
}    

In client project, programmer will register W3CAnalyzer to be injected into controller as WebStandardAnalyzer using his DI configurations (it doesn't really matter how).

The question

If I use some DI configuration and container in my library internally, how can I integrate this configuration with the client code, so there will be a possibility to register library class and all of its dependencies would have been resolved then? I don't know yet, how I will organize DI inside my library (this is actually the first part of the problem), I want to somehow use the php-di library mentioned above.

To summarize, how to organize DI inside non-framework reusable library, so that library could be used inside every other project (no matter which framework is used) and its (that library) dependencies would also be resolved during runtime of that project.

Thank you!

like image 717
Paweł Wrzeszcz Avatar asked Jul 22 '17 11:07

Paweł Wrzeszcz


People also ask

Which library is used for dependency injection?

Enterprise Library is a good example of a library that really takes advantage of a dependency injection container without being hard coupled to one.

What are the three types of dependency injection?

Types of DI There are three main styles of dependency injection, according to Fowler: Constructor Injection (also known as Type 3), Setter Injection (also known as Type 2), and Interface Injection (also known as Type 1).

How many ways can dependency injection be done?

There are three types of dependency injection — constructor injection, method injection, and property injection.


1 Answers

class W3CAnalyzerFactory
{
    public function build()
    {
         $Subject = new W3CAnalyzer(new HttpClient);

         return $Subject
    }
}

The above is how I make use of the Factory pattern, and had great success with it. If new HttpClient is not sufficient for your code (that is, it needs to be built, too), then do the following:

class HttpClientFactory
{
    public function build()
    {
         //modify this to actually construct the HttpClient object, with
         //constructor and/or setter injections
         $Subject = new HttpClient();

         return $Subject
    }
}

Then, your W3CAnalyzerFactory will look like:

class W3CAnalyzerFactory
{
    public function build()
    {
         $Subject = new W3CAnalyzer(
             (new HttpClientFactory)
             ->build()
         );

         return $Subject
    }
}

Then, someone using your library can do something like the following:

$WebStandardController = new WebStandardController(
    (new W3CAnalyzerFactory())
    ->build()
);

Or, however they want using their own DI setup. As you said, it doesn't matter how they inject it, just that they can using the W3CAnalyzerFactory class, and that everything is then ready to go.

Tip: For all objects in my library and/or application I always made a factory class, even if there was nothing injected. It feels wordy at first, but has a couple benefits.

  1. If you ever refactor a class (like HttpClient) so that it DOES now need to have something injected, then everything that uses HttpClientFactory is already done, and you don't need go through and replace all your new HttpClient() lines with (new HttpClientFactory)->build().
  2. You don't have to remember which of the injected implementations require a factory and which don't when building these factories. Everything does, so just use the factory. (i.e. Do I use new HttpClient or (new HttpClientFactory)->build()? Don't think about it, just use the factory.)

Creating these factories when you create the implementations is what I recommend, as it gets it out of the way and doesn't require you to come back later and try to think of how to string things together.

I think this approach is the simplest for dependency injection when you don't want to, or can't use a container to assist you.

Sidenote: The reason I assign the object to $Subject and then return $Subject a separate line instead of just doing return new Whatever... is because I tend to prefer setter injection, which means I need to do things to the object after it is constructed. You do what works best for your library and/or application though. The choice of the word "subject" as the variable name comes from calling my objects in my unit tests $SUT for "Subject Under Test". These aren't under test, so I just called them $Subject to have a nice consistent name that helps me easily see what object is the one being constructed. Again, that habit of mine is totally optional.

like image 165
Willem Renzema Avatar answered Oct 04 '22 00:10

Willem Renzema