I'm working on a framework (partly out of frustration with Zend, and partially as a learning exercise to improve my coding). I've constructed a fairly solid library of classes for data validation, and the next step was to string them all together with dependency injection.
However, I was finding it to be a bit laborious to assemble all the classes together into a unit capable of doing useful work. After doing a bit of digging around I found some stuff on DI Containers and figured that this might be a solution to the problem.
Here is the code I originally wrote to assemble my classes together into a unit for validation of a simple password + confirm password form.
use
gordian\reefknot\autoload\Autoload,
gordian\reefknot\input\validate,
gordian\reefknot\input\validate\type,
gordian\reefknot\input\validate\prop;
new Autoload ();
$password = new validate\Field (new type\IsString ());
$pwConf = new validate\Field (new type\IsString ());
$password -> addProp (new prop\Required ())
-> addProp (new prop\Min (array ('limit' => 5)))
-> addProp (new prop\Max (array ('limit' => 15)))
-> addProp (new prop\RegexMatch (array ('needle' => '/[a-zA-Z0-9_]{5,15}/')));
$pwConf -> addProp (new prop\Required ())
-> addProp (new prop\Equals (array ('value' => $password)));
$pwForm = new validate\DataSet (new type\IsArray ());
$pwForm -> addProp (new prop\Required ())
-> addProp (new prop\Min (array ('limit' => 2)))
-> addProp (new prop\Max (array ('limit' => 2)))
-> addField ('password', $password)
-> addField ('pwConf', $pwConf);
After reading up on DI frameworks, especially the slides here, I came up with the following DI Container implementation:
namespace gordian\reefknot\di;
class Container implements iface\Container
{
protected
$store = array ();
/**
* Add a new parameter to the container
*
* @param string $key
* @param mixed $value
* @return Container
*/
public function __set ($key, $value)
{
$this -> store [$key] = $value;
}
/**
* Retrieve an item from the container
*
* @param string $key
* @return mixed
* @throws \InvalidArgumentException if you attempt to access a non-existant key
*/
public function __get ($key)
{
$param = NULL;
if (isset ($this -> store [$key]))
{
$param = is_callable ($this -> store [$key])?
$this -> store [$key] ($this):
$this -> store [$key];
}
else
{
throw new \InvalidArgumentException ('Parameter ' . $key . 'not defined');
}
return ($param);
}
/**
* Retrieve the same instance of an object from the container.
*
* @param \Closure $callable
* @return Object
*/
public function single (\Closure $callable)
{
return (function ($c) use ($callable)
{
static $object = NULL;
if (is_null ($object))
{
$object = $callable ($c);
}
return ($object);
});
}
}
Unfortunately, this was the point at which stuff started going wrong, when I tried to use the container to construct the same form validation class assembly from above. I did get an assembly that worked in the end, but it takes up far more code than the original version did!
use
gordian\reefknot\autoload\Autoload,
gordian\reefknot\di\Container,
gordian\reefknot\input\validate,
gordian\reefknot\input\validate\type,
gordian\reefknot\input\validate\prop;
new Autoload ();
$container = new Container ();
// Configure params
$container -> formSize = 2;
$container -> pwMinLen = 5;
$container -> pwMaxLen = 15;
$container -> pwPattern = '/[a-zA-Z0-9_]{5,15}/';
// Configure types and props
$container -> IsString = function () {
return (new type\IsString ());
};
$container -> IsArray = function () {
return (new type\IsArray ());
};
$container -> Required = function () {
return new prop\Required ();
};
$container -> Min = function (Container $c) {
return new prop\Min (array ('limit' => $c -> pwMinLen));
};
$container -> Max = function (Container $c) {
return new prop\Max (array ('limit' => $c -> pwMaxLen));
};
$container -> Match = function (Container $c) {
return new prop\RegexMatch (array ('needle' => $c -> pwPattern));
};
$container -> Equals = function (Container $c) {
return new prop\Equals (array ('value' => $c -> password));
};
$container -> FormMin = function (Container $c) {
return new prop\Min (array ('limit' => $c -> formSize));
};
$container -> FormMax = function (Container $c) {
return new prop\Max (array ('limit' => $c -> formSize));
};
// Configure fields
$container -> password = $container -> single (function (Container $c) {
$field = new validate\Field ($c -> IsString);
$field -> addProp ($c -> Required)
-> addProp ($c -> Min)
-> addProp ($c -> Max)
-> addProp ($c -> Match);
return ($field);
});
$container -> pwConf = $container -> single (function (Container $c) {
$field = new validate\Field ($c -> IsString);
$field -> addProp ($c -> Required)
-> addProp ($c -> Equals);
return ($field);
});
$container -> form = $container -> single (function (Container $c) {
$form = new validate\DataSet ($c -> IsString);
$form -> addProp ($c -> Required)
-> addProp ($c -> FormMin)
-> addProp ($c -> FormMax)
-> addField ('password', $c -> password)
-> addField ('pwConf', $c -> pwConf);
return ($form);
});
$pwForm = $container -> form;
This seems like something of a backward step to me. As you can see, the version that uses the container takes up far more code than the version that just does raw DI for setup.
I'm sure that a DI framework can offer me a shortcut to building up dependency trees/graphs such as the one in my above example. However, this simple DI container doesn't look like it's the way to go about it.
Have I missed something in my use of the container that I need to address? Can a container like the one I implemented produce real benefits over raw DI?