A simple do-it-yourself PHP service container
In a previous post we talked about dependency injection, services, and dependency injection containers.
In this post we will implement a useful and yet simple implementation of a dependency injection container, or "service container".
It will not be very flexible or reusable, it will just do the job.
And your IDE will love it!
Use case
I often write these kinds of one-off containers for smaller local PHP projects or Drupal 7 modules, where
- All service definitions are known at the time we write the container.
- We don't expose the container as an API to the outside world.
- We mostly use it to manage "internal" services.
- We don't plan on reusing the container somewhere else. We might copy parts of it, but we don't design it as a reusable component.
- The environment (framework, etc) does not already provide a service container that we could use instead.
- In fact, the environment might be full of procedural code and global state, all the ugly stuff that we want to avoid.
We just want an island of sanity for our own stuff.
The goal is to organize our own code into small unit-testable components, and to use the container to manage the dependencies between these components.
Implementation
For the following steps we will use the same example as in the previous blog post: An application where we deal with restaurant-related stuff.
An IDE-friendly container signature.
With the Symfony2 service container, the way to request a service is to pass a string key to the get() method, like this:
<?php
$stove = $container->get('stove');
$stove->switchOn();
?>
Problem: The IDE does not know anything about the type of the service. Thus, it cannot verifiy that the $stove object actually has a switchOn() method. And, if we run "find usages" on the switchOn() method, we will get an empty result, even though the code above calls this method.
A smart way to solve this problem is to use magic __get() method in combination with the @property docblock.
<?php
/**
* @property StoveInterface $stove
* The stove used in the restaurant.
* @property CookInterface $cook
* The cook who works in the restaurant.
*/
class RestaurantServiceContainer {
function __get($key) {..}
}
$stove = $container->stove;
$stove->switchOn();
?>
Here the IDE can easily figure out that $stove is a StoveInterface object.
You often hear or read mixed opinions about PHP magic methods, but in this case it really brings a benefit.
Lazy instantiation and factory mapping.
The heart of the container will be the lazy instantiation mechanic.
When called with the machine name of a service, this mechanic has to check whether the service is already instantiated, and if not, map it to a factory method.
For simplicity's sake, the factory methods will be on the container itself, and there will be a simple mapping of service name to method name.
If the service name is "cook", then the factory method will be "create_cook()".
Note that other implementations (e.g. the Symfony2 container) use a different mapping of service name to method name.
We will implement this in an abstract base class.
If anything is going to be reusable then this!
<?php
abstract class AbstractServiceContainer {
/<strong>
* @var object[]
*/
private $services = array();
/</strong>
* Magic method that is triggered when someone calls $container->$name.
*
* @param string $name
* The machine name of the service.
* Must be a valid PHP identifier, without commas and such.
*
* @return object
*/
function __get($name) {
return isset($this->services[$name])
? $this->services[$name]
// Create the service, if it does not already exist.
: $this->services[$name] = $this->createService($name);
}
/**
* @param string $name
*
* @return object
* The service.
* @throws Exception
*/
private function createService($name) {
// Method to be implemented in a subclass.
$method = 'create_' . $name;
if (!method_exists($this, $method)) {
throw new \Exception("Unknown service '$name'.");
}
return $this->$method();
}
}
?>
Note that while we are going to subclass this, the $services property and the createService() method do not need to be accessible to the subclass. So we can safely declare them as private, instead of protected.
The RestaurantServiceContainer
Now we write the RestaurantServiceContainer subclass with the domain-specific methods and @property docblock tags.
<?php
/<strong>
* @property CookInterface cook
* @property StoveInterface stove
* @property KitchenInterface kitchen
* @property RestaurantInterface restaurant
*/
class RestaurantServiceContainer extends AbstractServiceContainer {
/</strong>
* @return Cook
*
* @see RestaurantServiceContainer::cook
*/
protected function create_cook() {
return new Cook();
}
/<strong>
* @return Stove
*
* @see RestaurantServiceContainer::stove
*/
protected function create_stove() {
return new Stove();
}
/</strong>
* @return Kitchen
*
* @see RestaurantServiceContainer::kitchen
*/
protected function create_kitchen() {
return new Kitchen($this->cook, $this->stove);
}
/**
* @return Restaurant
*
* @see RestaurantServiceContainer::restaurant
*/
protected function create_restaurant() {
return new Restaurant($this->kitchen);
}
}
?>
Putting it to use
Now that we have our container, we can actually use the services provided by it.
And what does a Restaurant object do? It handles a RestaurantVisitor :)
<?php
$restaurantContainer = new RestaurantContainer();
$visitor = new RestaurantVisitor();
$restaurantContainer->restaurant->handleVisitor($visitor);
?>
Container parameters
If this was too boring, we can let the factory methods depend on parameter values in the container.
E.g. one parameter could specify whether the stove factory method should create an electric stove or a gas stove.
<?php
/<strong>
* @property StoveInterface stove
*/
class RestaurantServiceContainer extends AbstractServiceContainer {
private $preferElectricStove;
/</strong>
* @param bool $preferElectricStove
* TRUE, for an electric stove.
* FALSE, for a gas stove.
*/
function __construct($preferElectricStove = TRUE) {
$this->preferElectricStove = $preferElectricStove;
}
/**
* @return StoveInterface
*
* @see RestaurantServiceContainer::stove
*/
protected function create_stove() {
return $this->preferElectricStove
? new ElectricStove()
: new GasStove();
}
}
?>
However: To keep this in line with the goals we defined before, we only want to add parameters where flexibility is actually needed!
We don't want to reinvent the Symfony2 container. We just want a one-off solution for a specific project.
Again, our IDE will love this code, and tell us that the handleVisitor() method does require a RestaurantVisitor argument.
Dedicated container variations for different scenarios
There can be cases where we need a different version of the container, where some of the services use a different implementation, or where some additional services are added and some of the regular services are not available.
E.g. the employees and work flows in the restaurant are different at night, when the kitchen is closed but the bar is open.
In this case, it can be useful to have a subclass of the RestaurantServiceContainer, where some factory methods are implemented differently.
In the "real world", we could have one version of the container for web requests in production, and another version for commandline processes.
Unit tests could each have their own versions of the container, to test only specific services and mock out the rest.
All of this may be covered in future articles..