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.
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 piece, 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 by creating a basic HTTP-based framework with the help of Disco and some Symfony Components.
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.
Disco is a container_interop compatible DI container. Somewhat controversially, Disco is an annotation-based DI container.
container_interop
's InterfacesNote 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 AcmeSampleService
*/
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 itExpertDiscoAnnotationBeanFactory;
use itExpertDiscoBeanFactoryRegistry;
// ...
// Setting up the container
$container = new AnnotationBeanFactory(Services::class, $config);
BeanFactoryRegistry::register($container);
Since Disco is container/interop
compatible, we can use get()
and has()
methods on the container object:
// ...
$sampleService = $container->get('sampleService');
$sampleService->callSomeMethod();
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.
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)
{
// ...
}
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.
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 this section, we'll write our framework's code along with code snippets to make some concepts clear.
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 SymfonyComponentHttpKernelHttpKernel;
use SymfonyComponentHttpKernelHttpKernelInterface;
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.
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 SymfonyComponentRoutingRouteCollection;
use SymfonyComponentRoutingRoute;
$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 SymfonyComponentRoutingRouteCollection;
use SymfonyComponentRoutingRoute;
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 SymfonyComponentRoutingRouteCollection;
use FrameworkRouteBuilder;
$routeBuilder = new RouteBuilder(new RouteCollection());
$routeBuilder
->get('home', '/', function() {
return new Response('It Works!');
})
->get('welcome', '/welcome', function() {
return new Response('Welcome!');
});
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 SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationRequestStack;
use SymfonyComponentHttpKernelEventListenerRouterListener;
use SymfonyComponentHttpKernelControllerControllerResolver;
// Create a request object from PHP's global variables
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/routes.php';
$UrlMatcher = new RoutingMatcherUrlMatcher($routes, new RoutingRequestContext());
// 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 FrameworkKernel($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 RoutingMatcherUrlMatcher($routes, new RoutingRequestContext());
// ...
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 FrameworkKernel($dispatcher, new ControllerResolver());
$response = $kernel->handle($request);
// Sending the response
$response->send();
// ...
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 bitExpertDiscoAnnotationsBean;
use bitExpertDiscoAnnotationsConfiguration;
use bitExpertDiscoAnnotationsParameters;
use bitExpertDiscoAnnotationsParameter;
/**
* @Configuration
*/
class Services {
/**
* @Bean
* @return SymfonyComponentRoutingRequestContext
*/
public function context()
{
return new SymfonyComponentRoutingRequestContext();
}
/**
* @Bean
*
* @return SymfonyComponentRoutingMatcherUrlMatcher
*/
public function matcher()
{
return new SymfonyComponentRoutingMatcherUrlMatcher($this->routeCollection(), $this->context());
}
/**
* @Bean
* @return SymfonyComponentHttpFoundationRequestStack
*/
public function requestStack()
{
return new SymfonyComponentHttpFoundationRequestStack();
}
/**
* @Bean
* @return SymfonyComponentRoutingRouteCollection
*/
public function routeCollection()
{
return new SymfonyComponentRoutingRouteCollection();
}
/**
* @Bean
* @return FrameworkRouteBuilder
*/
public function routeBuilder()
{
return new FrameworkRouteBuilder($this->routeCollection());
}
/**
* @Bean
* @return SymfonyComponentHttpKernelControllerControllerResolver
*/
public function resolver()
{
return new SymfonyComponentHttpKernelControllerControllerResolver();
}
/**
* @Bean
* @return SymfonyComponentHttpKernelEventListenerRouterListener
*/
protected function listenerRouter()
{
return new SymfonyComponentHttpKernelEventListenerRouterListener(
$this->matcher(),
$this->requestStack()
);
}
/**
* @Bean
* @return SymfonyComponentEventDispatcherEventDispatcher
*/
public function dispatcher()
{
$dispatcher = new SymfonyComponentEventDispatcherEventDispatcher();
$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 SymfonyComponentHttpFoundationRequest;
$request = Request::createFromGlobals();
$container = new itExpertDiscoAnnotationBeanFactory(Services::class);
itExpertDiscoBeanFactoryRegistry::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.
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 DotenvDotenv(__DIR__ . '/../Config');
$dotenv->load();
// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';
$container = new itExpertDiscoAnnotationBeanFactory(Services::class, $parameters); itExpertDiscoBeanFactoryRegistry::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.
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 itExpertDiscoAnnotationBeanFactory
*/
public static function buildContainer($parameters = [])
{
$container = new itExpertDiscoAnnotationBeanFactory(Services::class, $parameters);
itExpertDiscoBeanFactoryRegistry::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 SymfonyComponentHttpFoundationRequest;
// Getting the environment
$dotenv = new DotenvDotenv(__DIR__ . '/../config');
$dotenv->load();
// Load the proper configuration file based on the environment
$parameters = require __DIR__ . '/../config/' . getenv('ENV') . '.php';
$request = Request::createFromGlobals();
$container = FrameworkFactory::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?
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 SymfonyComponentHttpKernelHttpKernelInterface;
use SymfonyComponentHttpFoundationRequest;
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 FrameworkApplication
*/
public function application()
{
return new FrameworkApplication($this->kernel());
}
// ...
And this is the new look of our front controller:
<?php
require_once __DIR__ . '/../vendor/autoload.php';
// Getting the environment
$dotenv = new DotenvDotenv(__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 = FrameworkFactory::buildContainer($parameters);
// Including the routes
require __DIR__ . '/../src/routes.php';
// Running the application to handle the response
$app = $container->get('application')
->run();
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 SymfonyComponentEventDispatcherEventSubscriberInterface
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 SymfonyComponentEventDispatcherEventSubscriberInterface;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelEventGetResponseForControllerResultEvent;
use SymfonyComponentHttpKernelKernelEvents;
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 FrameworkStringResponseListener
*/
protected function ListenerStringResponse()
{
return new FrameworkStringResponseListener();
}
// ...
Then, we add it to the dispatcher service:
<?php
// ...
/**
* @Bean
* @return SymfonyComponentEventDispatcherEventDispatcher
*/
public function dispatcher()
{
$dispatcher = new SymfonyComponentEventDispatcherEventDispatcher();
$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.
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 piece was an attempt at providing a smooth start for those who might find this new kind of DI container interesting.
3.15.25.34