PHP
Article

Disco with Design Patterns: A Fresh Look at Dependency Injection

By Reza Lavaryan

Dependency Injection is all about code reusability. It’s a design pattern aiming to make high-level code reusable, by separating the object creation / configuration from usage.

Illustration of people's outlines dancing in a disco

Consider the following code:

<?php

class Test {

    protected $dbh;

    public function __construct(\PDO $dbh)
    {
        $this->dbh = $dbh;
    }

}

$dbh  = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
$test = new Test($dbh) 

As you can see, instead of creating the PDO object inside the class, we create it outside of the class and pass it in as a dependency – via the constructor method. This way, we can use the driver of our choice, instead of having to to use the driver defined inside the class.

Our very own Alejandro Gervasio has explained the DI concept fantastically, and Fabien Potencier also covered it in a series.

There’s one drawback to this pattern, though: when the number of dependencies grows, many objects need to be created/configured before being passed into the dependent objects. We can end up with a pile of boilerplate code, and a long queue of parameters in our constructor methods. Enter Dependency Injection containers!

A Dependency Injection container – or simply a DI container – is an object which knows exactly how to create a service and handle its dependencies.

In this article, we’ll demonstrate the concept further with a newcomer in this field: Disco.

For more information on dependency injection containers, see our other posts on the topic here.

As frameworks are great examples of deploying DI containers, we will finish the article by creating a basic HTTP-based framework with the help of Disco and some Symfony Components.

Installation

To install Disco, we use Composer as usual:

composer require bitexpert/disco

To test the code, we’ll use PHP’s built-in web server:

php -S localhost:8000 -t web

As a result, the application will be accessible under http://localhost:8000 from the browser. The last parameter -t option defines the document root – where the index.php file resides.

Getting Started

Disco is a container_interop compatible DI container. Somewhat controversially, Disco is an annotation-based DI container.

Note that the package container_interop consists of a set of interfaces to standardize features of container objects. To learn more about how that works, see the tutorial in which we build our own, SitePoint Dependency Injection Container, also based on container-interop.

To add services to the container, we need to create a configuration class. This class should be marked with the @Configuration annotation:

<?php
/**
 * @Configuration
 */
 class Services {
    // ...
 }

Each container service should be defined as a public or protected method inside the configuration class. Disco calls each service a Bean, which originates from the Java culture.

Inside each method, we define how a service should be created. Each method must be marked with @Bean which implies that this a service, and @return annotations which notes the type of the returned object.

This is a simple example of a Disco configuration class with one “Bean”:

<?php
/**
 * @Configuration
 */
class Configuration {

    /**
     * @Bean
     * @return SampleService
     */
    public function getSampleService()
    {
        // Instantiation
        $service  = new SampleService();

        // Configuration
        $service->setParameter('key', 'value');
        return $service; 
    }
}

The @Bean annotation accepts a few configuration parameters to specify the nature of a service. Whether it should be a singleton object, lazy loaded (if the object is resource-hungry), or even its state persisted during the session’s lifetime is specified by these parameters.

By default, all the services are defined as singleton services.

For example, the following Bean creates a singleton lazy-loaded service:

<?php

// ...

/**
 * @Bean({"singleton"=true, "lazy"=true})
 * @return \Acme\SampleService
 */
 public function getSampleService()
 {
     return new SampleService();
 }

// ...

Disco uses ProxyManager to do the lazy-loading of the services. It also uses it to inject additional behaviors (defined by the annotations) into the methods of the configuration class.

After we create the configuration class, we need to create an instance of AnnotationBeanFactory, passing the configuration class to it. This will be our container.

Finally, we register the container with BeanFactoryRegistry:

<?php

// ...

use \bitExpert\Disco\AnnotationBeanFactory;
use \bitExpert\Disco\BeanFactoryRegistry;

// ...

// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $config);
BeanFactoryRegistry::register($container);

How to Get a Service from the Container

Since Disco is container/interop compatible, we can use get() and has() methods on the container object:

// ...

$sampleService = $container->get('sampleService');
$sampleService->callSomeMethod();

Service Scope

HTTP is a stateless protocol, meaning on each request the whole application is bootstrapped and all objects are recreated. We can, however, influence the lifetime of a service by passing the proper parameters to the @Bean annotation. One of these parameters is scope. The scope can be either request or session.

If the scope is session, the service state will persist during the session lifetime. In other words, on subsequent HTTP requests, the last state of the object is retrieved from the session.

Let’s clarify this with an example. Consider the following class:

<?php

class sample {

    public $counter = 0;

    public function add()
    {
        $this->counter++;
        return $this;
    } 
}

In the above class, the value of $counter is incremented each time the add() method is called; now, let’s add this to the container, with scope set to session:

// ...
/**
 * @Bean({"scope"="session"})
 * @return Sample
 */
public function getSample()
{
    return new Sample();
}
// ...

And if we use it like this:

// ...
$sample = $container->get('getSample');
$sample->add()
       ->add()
       ->add();

echo $sample->counter; // output: 3
// ...

In the first run, the output will be three. If we run the script again (to make another request), the value will be six (instead of three). This is how object state is persisted across requests.

If the scope is set to request, the value will be always three in subsequent HTTP requests.

Container Parameters

Containers usually accept parameters from the outside world. With Disco, we can pass the parameters into the container as an associative array like this:

// ...
$parameters = [

    // Database configuration
    'database' => [        
        'dbms' => 'mysql',
        'host' => 'localhost',
        'user' => 'username',
        'pass' => 'password',
    ],
];

// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $parameters);
BeanFactoryRegistry::register($container);

To use these values inside each method of the configuration class, we use @Parameters and @parameter annotations:

<?php
// ...

/**
 * @Bean
 * @Parameters({
 *    @parameter({"name"= "database"})
 * })
 *
*/
public function sampleService($database = null)
{
    // ...
}

Disco in Action

In this section, we’re going to create a basic HTTP-based framework. The framework will create a response based on the information received from the request.

To build our framework’s core, we’ll use some Symfony Components.

HTTP Kernel

The heart of our framework. Provides the request / response basics.

Http Foundation

A nice object-oriented layer around PHP’s HTTP super globals.

Router

According to the official website: maps an HTTP request to a set of configuration variables – more on this below.

Event Dispatcher

This library provides a way to hook into different phases of a request / response lifecycle, using listeners and subscribers.

To install all these components:

composer require symfony/http-foundation symfony/routing symfony/http-kernel symfony/event-dispatcher

As a convention, we’ll keep the framework’s code under the Framework namespace.

Let’s also register a PSR-4 autoloader. To do this, we add the following namespace-to-path mapping under the psr-4 key in composer.json:

// ...
 "autoload": {
        "psr-4": {
            "": "src/"
        }
    }
// ...

As a result, all namespaces will be looked for within the src/ directory. Now, we run composer dump-autoload for this change to take effect.

Throughout the rest of the article, we’ll write our framework’s code along with code snippets to make some concepts clear.

The Kernel

The foundation of any framework is its kernel. This is where a request is processed into a response.

We’re not going to create a Kernel from scratch here. Instead, we’ll extend the Kernel class of the HttpKernel component we just installed.

<?php
// src/Framework/Kernel.php

namespace Framework;

use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class Kernel extends HttpKernel implements HttpKernelInterface {
}

Since the base implementation works just fine for us, we won’t reimplement any methods, and will instead just rely on the inherited implementation.

Routing

A Route object contains a path and a callback, which is called (by the Controller Resolver) every time the route is matched (by the URL Matcher).

The URL matcher is a class which accepts a collection of routes (we’ll discuss this shortly) and an instance of RequestContext to find the active route.

A request context object contains information about the current request.

Here’s how routes are defined by using the Routing component:

<?php

// ...

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$routes = new RouteCollection();

$routes->add('route_alias', new Route('path/to/match', ['_controller' => function(){
    // Do something here...
}]
));

To create routes, we need to create an instance of RouteCollection (which is also a part of the Routing component), then add our routes to it.

To make the routing syntax more expressive, we’ll create a route builder class around RouteCollection.

<?php
// src/Framework/RouteBuilder.php

namespace Framework;

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

class RouteBuilder {

    protected $routes;

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

    public function get($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'GET');
    }

    public function post($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'POST');
    }

    public function put($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'PUT');
    }

    public function delete($name, $path, $controller)
    {
        return $this->add($name, $path, $controller, 'DELETE');
    }

    protected function add($name, $path, $controller, $method)
    {
        $this->routes->add($name, new Route($path, ['_controller' => $controller], ['_method' => $method]));

        return $this;
    }

}

This class holds an instance of RouteCollection. In RouteBuilder, for each HTTP verb, there is a method which calls add(). We’ll keep our route definitions in the src/routes.php file:

<?php
// src/routes.php

use Symfony\Component\Routing\RouteCollection;
use Framework\RouteBuilder;

$routeBuilder = new RouteBuilder(new RouteCollection());

$routeBuilder

->get('home', '/', function() {
            return new Response('It Works!');
})

->get('welcome', '/welcome', function() {
            return new Response('Welcome!');
});

The Front Controller

The entry point of any modern web application is its front controller. It is a PHP file, usually named index.php. This is where the class autoloader is included, and the application is bootstrapped.

All the requests go through this file, and are from here dispatched to the proper controllers. Since this is the only file we’re going to expose to the public, we put it inside our web root directory, keeping the rest of the code outside.

<?php
//web/index.php

require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;

// Create a request object from PHP's global variables
$request = Request::createFromGlobals();

$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext());

// Event dispatcher & subscribers
$dispatcher = new EventDispatcher();
// Add a subscriber for matching the correct route. We pass UrlMatcher to this class
$dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack()));


$kernel = new Framework\Kernel($dispatcher, new ControllerResolver());
$response  = $kernel->handle($request);

// Sending the response
$response->send();

In the above code, we instantiate a Request object based on PHP’s global variables.

<?php
// ...
$request = Request::createFromGlobals();
// ...

Next, we load the routes.php file into $routes. Detecting the right route is the responsibility of the UrlMatcher class, so we create it, passing the route collection along with a RequestContext object.

<?php
// ...
$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new Routing\Matcher\UrlMatcher($routes, new Routing\RequestContext());
// ...

To use the UrlMatcher instance, we pass it to the RouteListener event subscriber.

<?php
// ...
// Event dispatcher & subscribers
$dispatcher = new EventDispatcher();
// Add a subscriber for matching the correct route. We pass UrlMatcher to this class
$dispatcher->addSubscriber(new RouterListener($UrlMatcher, new RequestStack()));
// ...

Any time a request hits the application, the event is triggered and the respective listener is called, which in turn detects the proper route by using the UrlMatcher passed to it.

Finally, we instantiate the kernel, passing in the Dispatcher and an instance of Controller Resolver – via its constructor:

<?php
// ...

$kernel    = new Framework\Kernel($dispatcher, new ControllerResolver());
$response  = $kernel->handle($request);

// Sending the response
$response->send();
// ...

Disco Time

So far we had to do plenty of instantiations (and configurations) in the front controller, from creating the request context object, the URL matcher, the event dispatcher and its subscribers, and of course the kernel itself.

It is now time to let Disco wire all these pieces together for us.

As before, we install it using Composer:

composer require bitexpert/Disco;

Then, we create the configuration class, and define the services we’ll need in the front controller:

<?php
// src/Framework/Services.php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameters;
use bitExpert\Disco\Annotations\Parameter;

/**
 * @Configuration
 */
class Services {

    /**
     * @Bean
     * @return \Symfony\Component\Routing\RequestContext 
     */
    public function context()
    {
        return new \Symfony\Component\Routing\RequestContext();
    }

    /**
     * @Bean
     *
     * @return \Symfony\Component\Routing\Matcher\UrlMatcher
     */
    public function matcher()
    {
        return new \Symfony\Component\Routing\Matcher\UrlMatcher($this->routeCollection(), $this->context());
    }

    /**
     * @Bean
     * @return \Symfony\Component\HttpFoundation\RequestStack
     */
    public function requestStack()
    {
        return new \Symfony\Component\HttpFoundation\RequestStack();
    }

    /**
     * @Bean
     * @return \Symfony\Component\Routing\RouteCollection
     */
    public function routeCollection()
    {
        return new \Symfony\Component\Routing\RouteCollection();
    }

    /**
     * @Bean
     * @return \Framework\RouteBuilder
     */
    public function routeBuilder()
    {
        return new \Framework\RouteBuilder($this->routeCollection());
    }

    /**
     * @Bean
     * @return \Symfony\Component\HttpKernel\Controller\ControllerResolver
     */
    public function resolver()
    {
        return new \Symfony\Component\HttpKernel\Controller\ControllerResolver();
    }


    /**
     * @Bean
     * @return \Symfony\Component\HttpKernel\EventListener\RouterListener
     */
    protected function listenerRouter()
    {
        return new \Symfony\Component\HttpKernel\EventListener\RouterListener(
            $this->matcher(),
            $this->requestStack()
        );
    }

    /**
     * @Bean
     * @return \Symfony\Component\EventDispatcher\EventDispatcher
     */
    public function dispatcher()
    {
        $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();

        $dispatcher->addSubscriber($this->listenerRouter());

        return $dispatcher;
    }

    /**
     * @Bean
     * @return Kernel
     */
    public function framework()
    {
        return new Kernel($this->dispatcher(), $this->resolver());
    }

}

Seems like a lot of code; but in fact, it’s the same code that resided in the front controller previously.

Before using the class, we need to make sure it has been autoloaded by adding it under the files key in our composer.json file:

// ...
 "autoload": {
        "psr-4": {
            "": "src/"
        },
        "files": [
            "src/Services.php"
        ]
    }
// ...

And now onto our front controller.

<?php

//web/index.php

require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$request   = Request::createFromGlobals();

$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class);
\bitExpert\Disco\BeanFactoryRegistry::register($container);

$routes   = include __DIR__.'/../src/routes.php';

$kernel   = $container->get('framework')
$response = $kernel->handle($request);
$response->send();

Now our front controller can actually breathe! All the instantiations are done by Disco when we request a service.

But How About the Configuration?

As explained earlier, we can pass in parameters as an associative array to the AnnotationBeanFactory class.

To manage configuration in our framework, we create two configuration files, one for development and one for the production environment.

Each file returns an associative array, which we can be loaded into a variable.

Let’s keep them inside Config directory:

// Config/dev.php

return [
    'debug' => true;
];

And for production:

// Config/prod.php

return [
    'debug' => false;
];

To detect the environment, we’ll specify the environment in a special plain-text file, just like we define an environment variable:

ENV=dev

To parse the file, we use PHP dotenv, a package which loads environment variables from a file (by default the filename is .env) into PHP’s $_ENV super global. This means we can get the values by using PHP’s getenv() function.

To install the package:

composer require vlucas/phpdotenv

Next, we create our .env file inside the Config/ directory.

Config/.env

ENV=dev

In the front controller, we load the environment variables using PHP dotenv:

<?php
//web/index.php

// ...

// Loading environment variables stored .env into $_ENV
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../Config');
$dotenv->load();

// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';

$container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters);       \bitExpert\Disco\BeanFactoryRegistry::register($container);

// ...

In the preceding code, we first specify the directory in which our .env file resides, then we call load() to load the environment variables into $_ENV. Finally, we use getenv() to get the proper configuration filename.

Creating a Container Builder

There’s still one problem with the code in its current state: whenever we want to create a new application we have to instantiate AnnotationBeanFactory in our front controller (index.php). As a solution, we can create a factory which creates the container, whenever needed.

<?php
// src/Factory.php

namespace Framework;

class Factory {

    /**
     * Create an instance of Disco container
     *
     * @param  array $parameters
     * @return \bitExpert\Disco\AnnotationBeanFactory
     */
    public static function buildContainer($parameters = [])
    {
        $container = new \bitExpert\Disco\AnnotationBeanFactory(Services::class, $parameters);
        \bitExpert\Disco\BeanFactoryRegistry::register($container);

        return $container;
    }

}

This factory has a static method named buildContainer(), which creates and registers a Disco container.

This is how it improves our front controller:

<?php
//web/index.php

require_once __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

// Getting the environment
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../config');
$dotenv->load();

// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';

$request   = Request::createFromGlobals();
$container = Framework\Factory::buildContainer($parameters);
$routes    = include __DIR__.'/../src/routes.php';

$kernel   = $container->get('framework')
$response = $kernel->handle($request);
$response->send();

It looks much neater now, doesn’t it?

Application Class

We can take things one step further in terms of usability, and abstract the remaining operations (in the front controller) into another class. Let’s call this class Application:

<?php

namespace Framework;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;

class Application {

    protected $kernel;

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

    public function run()
    {
        $request = Request::createFromGlobals();

        $response = $this->kernel->handle($request);
        $response->send();
    }
}

Application is dependent on the kernel, and works as a wrapper around it. We create a method named run(), which populates the request object, and passes it to the kernel to get the response.

To make it even cooler, let’s add this class to the container as well:

<?php

// src/Framework/Services.php

// ...

    /**
     * @Bean
     * @return \Framework\Application
     */
    public function application()
    {
        return new \Framework\Application($this->kernel());
    }

// ...

And this is the new look of our front controller:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

// Getting the environment
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../config');
$dotenv->load();

// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';

// Build a Disco container using the Factory class
$container = Framework\Factory::buildContainer($parameters);

// Including the routes
require __DIR__ . '/../src/routes.php';

// Running the application to handle the response
$app = $container->get('application')
          ->run();

Creating a Response Listener

We can use the framework now, but there is still room for improvement. Currently, we have to return an instance of Response in each controller, otherwise, an exception is thrown by the Kernel:

<?php

// ...

$routeBuilder

->get('home', '/', function() {
            return new Response('It Works!');
});

->get('welcome', '/welcome', function() {
            return new Response('Welcome!');
});

// ...

However, we can make it optional and allow for sending back pure strings, too. To do this, we create a special subscriber class, which automatically creates a Response object if the returned value is a string.

Subscribers must implement the Symfony\Component\EventDispatcher\EventSubscriberInterface interface. They should implement the getSubscribedMethods() method in which we define the events we’re interested in subscribing to, and their event listeners.

In our case, we’re interested in the KernelEvents::VIEW event. The event happens when a response is to be returned.

Here’s our subscriber class:

<?php
// src/Framework/StringResponseListener
namespace Framework;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class StringResponseListener implements EventSubscriberInterface
{
    public function onView(GetResponseForControllerResultEvent $event)
    {
        $response = $event->getControllerResult();

        if (is_string($response)) {
            $event->setResponse(new Response($response));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(KernelEvents::VIEW => 'onView');
    }
}

Inside the listener method onView, we first check if the response is a string (and not already a Response object), then create a response object if required.

To use the subscriber, we need to add it to the container as a protected service:

<?php

// ...

/**
 * @Bean
 * @return \Framework\StringResponseListener
 */
protected function ListenerStringResponse()
{
    return new \Framework\StringResponseListener();
}

// ...

Then, we add it to the dispatcher service:

<?php

// ...

/**
 * @Bean
 * @return \Symfony\Component\EventDispatcher\EventDispatcher
 */
public function dispatcher()
{
    $dispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();

    $dispatcher->addSubscriber($this->listenerRouter());
    $dispatcher->addSubscriber($this->ListenerStringResponse());

    return $dispatcher;
}

// ...

From now on, we can simply return a string in our controller functions:

<?php

// ...

$routeBuilder

->get('home', '/', function() {
            return 'It Works!';
})

->get('welcome', '/welcome', function() {
            return 'Welcome!';
});

// ...

The framework is ready now.

Conclusion

We created a basic HTTP-based framework with the help of Symfony Components and Disco. This is just a basic Request/Response framework, lacking any other MVC concepts like models and views, but allows for the implementation of any additional architectural patterns we may desire.

The full code is available on Github.

Disco is a newcomer to the DI-container game and if compared to the older ones, it lacks a comprehensive documentation. This article was an attempt at providing a smooth start for those who might find this new kind of DI container interesting.

Do you glue together your app’s components with DI containers? If so, which ones? Have you given Disco a try? Let us know!

  • http://www.skooppa.com s.molinari

    Out of curiosity, how does Disco persist the state in the session scoped beans?

    Scott

    • Stephan Hochdörfer

      Session scoped beans will be “stored” in the generated configuration class. All you need to do yourself is serialize the AnnotationBeanFactory. Dependencies of session scoped beans get turned into Lazy dependencies and thus will be fetched again from the AnnotationBeanFactory when the session scoped bean gets re-created.

      • http://www.skooppa.com s.molinari

        So, the persistence is done within the file system? I mean, PHP doesn’t save anything itself between requests, right?

        Scott

  • Stephan Hochdörfer

    Thank you very much for the in-depth coverage of Disco. Would you mind helping us to improve the documentation? We know it’s not the best out there but due to the lack of time we could not yet come up with a better version.

    The predecessor of Disco might be one of the two oldest DI containers in the PHP world. We started the development in 2005 / 2006 but kept it internal for quite a long time. 2 years ago I decided to drop XML configuration support and move to a PHP only configuration language and thus Disco came to light. All the feature Disco currently has come from the early days of 2005 / 2006.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.