Using pimple as my DI container, I have been bravely refactoring small classes to rely on DI injection, eliminating the hard-coded dependencies I could see would be easily removed.
My methodology for this task is very simple, but I have no clue if it is proper as I have very little experience with DI and Unit-testing aside from what I learned here in past month.
I created a class, ContainerFactory that subclasses pimple, and in that subclass have created methods that simply returns container for specific object.
The constructor calls the proper creator method depending on type:
function __construct($type=null, $mode = null){
if(isset($type)){
switch ($type) {
case 'DataFactory':
$this->buildDataFactoryContainer($mode);
break;
case 'DbConnect':
$this->buildDbConnectContainer($mode);
break;
default:
return false;
}
}
}
The method signature for container object creation is as follows:
public function buildDataFactoryContainer($mode=null)
The idea being that I can set $mode to test when calling this container, and have it load test values instead of actual runtime settings. I wanted to avoid writing separate container classes for testing, and this is an easy way I thought to not have test related code all over.
I could instead have subclassed ContainerFactory ie: ContainerFactoryTesting extends ContainerFactory
and override in that instead of mixing test code with app code and cluttering method signatures with $mode=null, but that is not the point of this post. Moving on, to create a container for a particular object, I simply do this:
// returns container with DataFactory dependencies, holds $db and $logger objects.
$dataFactoryContainer = new ContainerFactory('DataFactory');
// returns container with test settings.
$dataFactoryTestContainer = new ContainerFactory('DataFactory','test');
// returns container with DbConnect dependencies, holds dbconfig and $logger objects.
$dbConnectContainer = new ContainerFactory('DbConnect');
I just ran into an issue that makes me suspect that the strategy I am building upon is flawed.
Looking at the above, DataFactory contains $db object that holds database connections. I am now refactoring out this dbclass to remove its dependencies on a $registry object, but how will I create $dataFactoryContainer, when I add $db object that needs $dbConnectContainer?
For example, in datafactory container I add a dbconnect instance, but IT will now need a container passed to it ...
I realise my English is not that great and hope I have explained well enough for a fellow SO'er to understand.
My question is two-fold, how do you guys handle creating objects for dependencies that themselves contain dependencies, in a simple manner?
And .. how do you separate container configuration for creation of objects for testing purposes?
As always, any comment or link to relevant post appreciated.
You should not create separate containers for everything, but instead use a single container. You can create a container "Extension" which basically just sets services on your container.
Here is an extensive example of my suggested setup. When you have a single container, recursively resolving dependencies is trivial. As you can see, the security.authentication_provider
depends on db
, which depends on db.config
.
Due to the laziness of closures it's easy to define services and then define their configuration later. Additionally it's also easy to override them, as you can see with the TestExtension.
It would be possible to split your service definitions into multiple separate extensions to make them more reusable. That is pretty much what the Silex microframework does (it uses pimple).
I hope this answers your questions.
ExtensionInterface
class ExtensionInterface
{
function register(Pimple $container);
}
AppExtension
class AppExtension
{
public function register(Pimple $container)
{
$container['mailer'] = $container->share(function ($container) {
return new Mailer($container['mailer.config']);
});
$container['db'] = $container->share(function ($container) {
return new DatabaseConnection($container['db.config']);
});
$container['security.authentication_provider'] = $container->share(function ($container) {
return new DatabaseAuthenticationProvider($container['db']);
});
}
}
ConfigExtension
class ConfigExtension
{
private $config;
public function __construct($configFile)
{
$this->config = json_decode(file_get_contents($configFile), true);
}
public function register(Pimple $container)
{
foreach ($this->config as $name => $value) {
$container[$name] = $container->protect($value);
}
}
}
config.json
{
"mailer.config": {
"username": "something",
"password": "secret",
"method": "smtp"
},
"db.config": {
"username": "root",
"password": "secret"
}
}
TestExtension
class TestExtension
{
public function register(Pimple $container)
{
$container['mailer'] = function () {
return new MockMailer();
};
}
}
ContainerFactory
class ContainerFactory
{
public function create($configDir)
{
$container = new Pimple();
$extension = new AppExtension();
$extension->register($container);
$extension = new ConfigExtension($configDir.'/config.json');
$extension->register($container);
return $container;
}
}
Application
$factory = new ContainerFactory();
$container = $factory->create();
Test Bootstrap
$factory = new ContainerFactory();
$container = $factory->create();
$extension = new TestExtension();
$extension->register($container);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With