DEV Community 👩‍💻👨‍💻

Sarfraz Ahmed
Sarfraz Ahmed

Posted on • Originally published at codeinphp.github.io on

From Bad to Good: DI, DIC and Services

In this post, I intend to touch the concepts of:

Even though these terms seems scary at first especially to new-comers but are really easy to understand and help write quality code that is easy to understand and extend. We will take a ^legacy code^ and convert it into object-oriented one by using these principles.

Suppose we have simple application that sends newsletter email to all its subscribers, here is the legacy code:

// file: index.php or could also be considered as Controller file in MVC paradigm

require __DIR__. 'vendor/autoload.php';

use Demo\Mailer\SmtpMailer;

$dsn = 'sqlite:' . __DIR__. 'database.sqlite';
$pdo = new PDO($dsn);

$mailer = new SmtpMailer('smtp.example.com', 'user', 'password', '465');

$sql = 'SELECT * from subscribers';

foreach ($pdo->query($sql) as $row) {
    $mailer->sendMessage(
        $row['email'],
        'This is the email body',
        $row['name'],
        'info@example.com'
    );
}

Right now above code smells and has these problems:

  • Logic is mixed up, SQL queries, sending emails database configuration is all in one file

To fix this, first instinct is to convert it into class:

class SubscriberMailer
{
    public function sendEmails()
    {
        $dsn = 'sqlite:' . __DIR__. 'database.sqlite';
        $pdo = new PDO($dsn);

        $mailer = new SmtpMailer('smtp.example.com', 'user', 'password', '465');

        $sql = 'SELECT * from subscribers';

        foreach ($pdo->query($sql) as $row) {
            $mailer->sendMessage(
                $row['email'],
                'This is the email body',
                $row['name'],
                'info@example.com'
            );
        }
    }
}

This looks good to eye but it has more problems:

  • Logic is still mixed up, SQL queries, sending emails database configuration is all in one file
  • It has hard-coded dependencies (tight-coupling) on PDO and Mailer class, what if we wanted to change Mailer class ?
  • It is hard to unit-test because of tight coupling

Let's try to fix these problems. We can see that this class has dependency on PDO so instead of hard-coding it inside SubscriberMailer class, we can ^pass^ it via its constructor which now makes the class:

class SubscriberMailer
{
    protected $pdo = null;

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

    public function sendEmails()
    {
        $mailer = new SmtpMailer('smtp.example.com', 'user', 'password', '465');

        $sql = 'SELECT * from subscribers';

        foreach ($this->pdo->query($sql) as $row) {
            $mailer->sendMessage(
                $row['email'],
                'This is the email body',
                $row['name'],
                'info@example.com'
            );
        }
    }
}

This is a bit better, the class doesn't care where PDO objects comes from, it just needs one to operate and you can't instantiate the class without providing one so it makes its intention clear. So this is what is called Dependency Injection , no need to fear even though the term sounded scary initially (read my previous article on more ways of injecting dependencies). Here is how we use above class at this point of the time:

require __DIR__. 'vendor/autoload.php';

use Demo\Mailer\SmtpMailer;
use Demo\Mailer\SubscriberMailer;

$dsn = 'sqlite:' . __DIR__. 'database.sqlite';
$pdo = new PDO($dsn);

$subscriberMailer = new SubscriberMailer($pdo);
$subscriberMailer->sendEmails();

This is starting to look better compared to first snippet of code. However, we still have a problem in that we have hard-coded smtp config into our class, what if we wanted to change the smtp config without touching the class ? Or what if we had different setup of smtp config for development and production ? Remember a class should be a black-box which simply does its job without editing it again and again, you can only extend it not edit it. So this reasoning hints us that like pdo, we should also pass smtp config via class's constructor and move it from the class itself.

class SubscriberMailer
{
    protected $pdo = null;
    protected $smtpConfig = [];

    public function __construct($pdo, array $smtpConfig)
    {
        $this->pdo = $pdo;
        $this->smtpConfig = $smtpConfig;
    }

    public function sendEmails()
    {
        $mailer = new SmtpMailer(
            $this->smtpConfig['host'],
            $this->smtpConfig['user'],
            $this->smtpConfig['password'],
            $this->smtpConfig['port']
        );

        $sql = 'SELECT * from subscribers';

        foreach ($this->pdo->query($sql) as $row) {
            $mailer->sendMessage(
                $row['email'],
                'This is the email body',
                $row['name'],
                'info@example.com'
            );
        }
    }
}

Usage:

require __DIR__. 'vendor/autoload.php';

use Demo\Mailer\SmtpMailer;
use Demo\Mailer\SubscriberMailer;

$dsn = 'sqlite:' . __DIR__. 'database.sqlite';
$pdo = new PDO($dsn);

$subscriberMailer = new SubscriberMailer($pdo, [
   'host' => 'smtp.example.com',
   'user' => 'user',
   'password' => 'password',
   'port' => '465',
]);

$subscriberMailer->sendEmails();

This is better, we can now readily change smtp config in one place and we don't have to touch our class. It still has these problems though:

  • It is tightly coupled to SMTP mailer, what if we wanted to change mailer from smtp to another one like sendmail ?
  • SQL is still written in our class, logic is mixed up, our mail class shouldn't know about users or interact with database, it just needs to send emails !

Hint: The new keyword is considered code-smell, whenever you encounter it, it means it is something you should pass via dependency injection.

So by now we know that to remove hard-coded dependency or tight-coupling, we should pass it instead via dependency injection. So instead of passing mailer config, we should pass mailer object:

class SubscriberMailer
{
    protected $pdo = null;
    protected $mailer = null;

    public function __construct(\PDO $pdo, SmtpMailer $mailer)
    {
        $this->pdo = $pdo;
        $this->mailer = $mailer;
    }

    public function sendEmails()
    {
        $sql = 'SELECT * from subscribers';

        foreach ($this->pdo->query($sql) as $row) {
            $this->mailer->sendMessage(
                $row['email'],
                'This is the email body',
                $row['name'],
                'info@example.com'
            );
        }
    }
}

Usage:

require __DIR__. 'vendor/autoload.php';

use Demo\Mailer\SmtpMailer;
use Demo\Mailer\SubscriberMailer;

$dsn = 'sqlite:' . __DIR__. 'database.sqlite';
$pdo = new PDO($dsn);

$mailer = new SmtpMailer('smtp.example.com', 'user', 'password', '465');

$subscriberMailer = new SubscriberMailer($pdo, $mailer);       
$subscriberMailer->sendEmails();

This is better, we removed hard-coded dependency to smtp mailer and notice that we have also type-hinted the PDO and SmtpMailer in the constructor of our class, this is extremely helpful because:

  • Our class makes clear intention that it needs a PDO and SmtpMailer to work properly
  • If somebody passes something other than these two dependencies, they would get clear error message
  • Type-hinting makes our code self-documenting
  • It makes IDE auto-completion

Right now our mailer class knows it needs an instance of SmtpMailer to work and it should have a sendMessage method. Remember, we refactored our code in hope that we might use some other type of mailer like sendmail but problem is that it might not necessarily have the sendMessage method which means we cannot use it. So how do we solve this problem to be able to use some other type of mailer implementation ? Well the answer is to use an interface/contract and use that instead:

// app/Demo/Contracts/MailerInterface.php
interface MailerInterface
{
   public function sendMessage($email, $body, $subject, $from);
}

// app/Demo/Mailer/SmtpMailer.php
use Demo\Contracts\MailerInterface;

class SmtpMailer implements MailerInterface
{
    public function sendMessage($email, $body, $subject, $from) {
        // code to send email
    }
}

// app/Demo/Mailer/SubscriberMailer.php    
class SubscriberMailer
{
    protected $pdo = null;
    protected $mailer = null;

    public function __construct(\PDO $pdo, MailerInterface $mailer)
    {
        $this->pdo = $pdo;
        $this->mailer = $mailer;
    }

    public function sendEmails()
    {
        $sql = 'SELECT * from subscribers';

        foreach ($this->pdo->query($sql) as $row) {
            $this->mailer->sendMessage(
                $row['email'],
                'This is the email body',
                $row['name'],
                'info@example.com'
            );
        }
    }
}

Just like every step that we are performing, this step also has nice advantages:

  • Our SubscriberMailer class is now greatly expandable since it now accepts ANY object that implements MailerInterface that has sendMessage method
  • It documents our code a bit more, it is clear now how an object should be constructed that implements the MailerInterface

All well and good so far. We now see that in our class has SQL to get users. Again it should not care about talking to database, it just needs users' emails to send emails to. To fix this, we need use the Repository Pattern which is essentially a class talking to database to manipulate specific entity, in this case users. So we can pass UserRepository as dependency that has a method to get all users:

// app/Demo/Mailer/SubscriberMailer.php
use Demo\Contracts\MailerInterface;
use Demo\Repositories\UserRepository;

class SubscriberMailer
{
    protected $mailer = null;
    protected $userRepository = null;

    public function __construct(MailerInterface $mailer, UserRepository $userRepository)
    {
        $this->mailer = $mailer;
        $this->userRepository = $userRepository;
    }

    public function sendEmails()
    {
        $users = $this->userRepository->getAll();

        foreach ($users as $user) {
            $this->mailer->sendMessage(
                $user->email,
                'This is the email body',
                $user->name,
                'info@example.com'
            );
        }
    }
}

Usage:

require __DIR__. 'vendor/autoload.php';

use Demo\Mailer\SmtpMailer;
use Demo\Mailer\SubscriberMailer;
use Demo\Repositories\UserRepository;

$mailer = new SmtpMailer('smtp.example.com', 'user', 'password', '465');

$subscriberMailer = new SubscriberMailer($mailer, new UserRepository);    
$subscriberMailer->sendEmails();

Awesome! our SubscriberMailer class now does not have any mixed-up logic, it is complete black-box whose task is to send emails to provided users. It doesn't need to care about how you pass MailerInterface or UserRepository, it just needs them to operate. It has only one task of sending emails. Not to mention that our class is also easy to be unit-tested. Notice also that we removed PDO as dependency since it is now job of UserRepository to give us the users.

We can now call our class a Service that performs one and only one task without any coupling. If you have heard about Service-Oriented Architecture, this is what it is, a collection of service classes with one and only one responsibility each without any hard-coded dependencies.

At this point of the time, we are all good with our class but one of the downsides of Dependency Injection is that all the complexity of creating and configuring objects is your job. Now this isn't that bad since it happens at one place and gives you the control but it is something which we can still improve.

To do this, we need to leverage what is called Dependency Injection Container (DIC). The job of DIC is to create and configure objects for us. There are many DIC out there for PHP but let's see our modified code using famous Pimple container, here we wrap the dependencies or services into Pimple container. I am not going into details of how to use it, you should refer to its documentation.

// file: container.php

use Demo\Mailer\SmtpMailer;
use Demo\Repositories\UserRepository;
use Pimple\Container;
use Demo\Mailer\SubscriberMailer;

$container = new Pimple();

// container can also hold configs - ideally should be put in separate file...
$container['smtp_config'] = [
   'host' => 'smtp.example.com',
   'user' => 'user',
   'pass' => 'password',
   'port' => '465',
];

$container['mailer'] = $container->share(function(Pimple $container){
   return new SmtpMailer(
      $container['smtp_config']['host'],
      $container['smtp_config']['user'],
      $container['smtp_config']['pass'],
      $container['smtp_config']['port']
   );
});

$container['userRepository'] = $container->share(function(){
   return new UserRepository();
});

$container['subscriberMailer'] = $container->share(function(Pimple $container){
   return new SubscriberMailer($container['mailer'], $container['userRepository']);
});

// file: index.php

require __DIR__. 'vendor/autoload.php';
require __DIR__. 'container.php';

$subscriberMailer = $container['subscriberMailer'];
$subscriberMailer->sendEmails();

Notice how clear and concise our index.php code is now compared to initial version.

Using the DIC provides us with these advantages:

  • Wrapping our object creation in container's anonymous function makes object creation lazy which means object isn't created only until needed and if we never reference our SubscriberMailer class, those objects are never created at all saving us memory !
  • Using the container's share method means no matter how many times we need, it will always give us same original object instance. If we need to send many emails, we don't need many instances. This also makes code a bit more faster.
  • All the logic of which objects depend on other objects is abstracted away into container itself, we don't need to worry how they are created or what their dependencies are, we just use our services straight away out of container.
  • If we ever need to change config or something, we only edit it at one place, nobody needs to know about this change.

In conclusion, no matter which framework you use, these principles can be applied to make code which is easy to understand, extend and test.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.