cult3

How to resolve environment specific implementations from Laravel's IoC Container

Aug 31, 2015

Table of contents:

  1. The scenario
  2. Add the config details
  3. Creating the Interface Contract
  4. Create the Concrete Implementations
  5. Create the Manager class
  6. Add a Service Provider
  7. Conclusion

A good web framework deals with the plumbing of the application so you can concentrate on what your application should do best.

Laravel provides a whole boatload of conveniences that make developing web applications a lot easier.

One of the nice conventions of Laravel is the ability to resolve environment specific implementations.

This means you can use one implementation for development, and a whole different implementation for production.

For example, in development you probably just want to log sending emails, whilst in production you will want to use an implementation that will actually send the emails. This will save you from accidentally emailing your customers when testing your code.

Laraval provides the foundation for this functionality within the framework. This means we can very easily create the same conveniences for our own services and get the benefit of environment-based implementations.

In today’s tutorial we’re going to be looking at resolving different implementations for the different environments of the application.

The scenario

In order to see the benefit of this technique, it’s useful to set up a realistic scenario.

Imagine when a user signs up to the application we would like to send them an SMS to confirm their account.

We are going to be using Twilio to send the SMS messages to the user.

However, we don’t want to tie ourself to Twilio. In the unlikely scenario that a competitor comes a long, we want to be able to switch the implementation without the hassle of combing through the entire code base.

We also don’t want to send SMS messages in the development or testing environments as that will waste the company money and potentially be embarrassing if customers receive those messages.

Instead, we need to be able to resolve environment specific implementations. By specifying a “driver” in each environment we can resolve that implementation from the IoC container.

Each implementation should be interchangeable and so as we switch implementations between environments, nothing should need to change.

Laravel already has this functionality built into the framework. You can see it in action for the Mail, Cache, and Queue services.

So instead of bootstrapping this functionality ourselves, we can simply build upon the existing foundation.

Add the config details

The first thing we need to do is to create a new messaging.php config file:

return [
    "driver" => env("SMS_DRIVER", "log"),
];

This will check the environment variable for SMS_DRIVER. If the environment variable is not set, the default implementation will be log.

You can also add your Twilio credentials to the services.php configuration file.

Creating the Interface Contract

Next we need to write the Interface Contract of the Messaging Service.

Each implementation should be interchangeable, and so each should comply with the contract. This means we don’t need to care which implementation we are working with as they should all behave in the same way:

interface Messenger
{
    /**
     * Send a Message
     *
     * @param Message $message
     * @return Message
     */
    public function send(Message $message);
}

You can read more about Interfaces in PHP in When should I code to an Interface?.

This is going to be a really simple example so we only need a single send() method that accepts the Message value object.

Create the Concrete Implementations

Next we will create the concrete implementations.

First we will create a LogMessenger that will simply write to the Log. This will be useful for developing with because we don’t want to actually sent SMS messages, but we do want to inspect the log to make sure everything was working correctly:

class LogMessenger implements Messenger
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @param LoggerInterface $logger
     * @return void
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Send the Message
     *
     * @param Message $message
     * @return Message
     */
    public function send(Message $message)
    {
        $this->logger->debug("Messenger:", $message->toArray());

        $message->sent();

        return $message;
    }
}

Next we can create the Twilio implementation that will be used in production:

class TwilioMessenger implements Messenger
{
    /**
     * @var string
     */
    private $id;

    /**
     * @var string
     */
    private $token;

    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * @var string
     */
    private static $endpoint = "..";

    /**
     * @param string $id
     * @param string $secret
     * @param ClientInterface $client
     * @return void
     */
    public function __construct($id, $secret, ClientInterface $client)
    {
        $this->id = $id;
        $this->secret = $secret;
        $this->client = $client;
    }
    /**
     * Send the Message
     *
     * @param Message $message
     * @return Message
     */
    public function send(Message $message)
    {
        $response = $this->client->post(self::$endpoint, $message);

        $message->sent();

        return $message;
    }
}

Note: This is just a hypothetical example for illustrative purposes. I’m also not including tests as testing isn’t really the important part of this tutorial. You should be testing your implementations.

Create the Manager class

Next we need to create the Manager class that will determine which implementation to resolve for the given environment.

Laravel provides a Manager class that we can extend. You can see this abstract class here.

First create a class that extends the Manager class:

use Illuminate\Support\Manager;

class MessengerManager extends Manager
{
}

To add new drivers, you simply add methods to this class where the method name is create___Driver.

So for example, here is my LogMessenger method:

/**
 * Create an instance of the Log Messenger driver
 *
 * @return LogMessenger
 */
protected function createLogDriver()
{
    return new LogMessenger($this->app->make('Psr\Log\LoggerInterface'));
}

Lastly, we need to add a method to return the default driver from the config file we set up earlier:

/**
 * Get the default driver
 *
 * @return string
 */
public function getDefaultDriver()
{
    return $this->app['config']['messaging.driver'];
}

Add a Service Provider

Finally we can add a Service Provider to resolve the implementation from the IoC:

class MessengerServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the Messenger Service
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton("Cribbb\Messaging\Messenger", function ($app) {
            $manager = new MessengerManager($app);

            return $manager->driver();
        });
    }
}

First we create a new instance of the MessengerManager class and inject an instance of $app.

Next we can resolve the correct instance by calling the driver() method.

Now when you resolve an implementation from the IoC container, you will be given the correct implementation for that environment.

Conclusion

I really like Laravel’s system for resolving environment specific implementations for important service classes. It’s easy to understand and set up and you can feel safe in the knowledge that the correct implementation will be resolved in each specific environment.

As you can see from this tutorial, we didn’t have to invent anything new to make this work. By simply extending the native Manager class we can very easily provide this kind of functionality for our application specific services.

This also promotes good practices by coding to an interface to ensure that each implementation is interchangeable between environments.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.