5
\$\begingroup\$

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?

\$\endgroup\$
3
  • \$\begingroup\$ Will look through this later, but I sent this to the guy who made the Zend DI Container :) He says github.com/ralphschindler/Zend_DI-Examples \$\endgroup\$ Mar 6 '12 at 22:42
  • \$\begingroup\$ I think you are confusing "Dependency Injection" with "dependency Injection Container" \$\endgroup\$
    – tereško
    Mar 12 '12 at 20:00
  • \$\begingroup\$ @teresko: The first example is pure DI (for want of a better term) and works OK but is a bit verbose. I was hoping to implement a DI container (second code block) to make life easier. But it actually ended up in a lot more code to achieve the same goal (third code block). \$\endgroup\$
    – GordonM
    Mar 12 '12 at 20:23
1
+50
\$\begingroup\$

I think the real problem here is your over-zealousness of using classes.

Consider this snippet of your code:

$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}/')));

Why can't it be:

$password   -> addProperties ([
                   'required' => true,
                   'min' => 5,
                   'max' => 15,
                   'rule' => '/[a-zA-Z0-9_]{5,15}/'
               ]);

One of the main goals of any framework should be to make your life easier. You are adding complication for hardly anything gained, and at the expense of more keystrokes.

\$\endgroup\$
6
  • \$\begingroup\$ That's more or less exactly what similar code in Zend Framework would look, and whilst it's okay for simpler cases I've found ZF's frustration level to rise exponentially with the complexity of what you're trying to achieve with it, especially when Decorators are thrown into the mix... (shudder). This is mostly an exercise in exploring alternative approaches. \$\endgroup\$
    – GordonM
    Mar 12 '12 at 20:25
  • \$\begingroup\$ I fail to see how $password->addDecorator($decorator) wouldn't work . . . \$\endgroup\$ Mar 12 '12 at 20:28
  • \$\begingroup\$ I was just using decorators as an example of where Zend gets to be painful. :) I'm keen to avoid code like your example because it would mean that somewhere inside the Field class there would have to be code to instantiate classes of whichever type is specified in the array. That would complicate unit testing. In this particular case though, I expect just doing it that way would be easier. The whole thing is after all meant to be a learning exercise and learning when not to use pattern X is certainly part of that. :) \$\endgroup\$
    – GordonM
    Mar 12 '12 at 20:53
  • \$\begingroup\$ I was really just hoping for an answer along the lines of either "DI container XYZ could assemble your classes in a cleaner way, you might want to look at how that works", or "Your DI container works pretty much how the others do, but if you use it in XYZ way you'll find it more useful", or even "No DI container is going to help make your code less verbose, just give it up" :) \$\endgroup\$
    – GordonM
    Mar 12 '12 at 20:54
  • \$\begingroup\$ After giving it a lot of thought I'm thinking that a Builder class of some sort would be a good compromise here, you'd pass it in an array structure like the one in your example and it would do all the instantiation and wiring up of the relevant classes. That should hopefully cut down on keystrokes whilst still allowing each class to be easily unit tested in isolation/reused/etc. So as you gave me the idea and as it's unlikely that anyone else is going to answer before the bounty expires you get the bounty. \$\endgroup\$
    – GordonM
    Mar 13 '12 at 8:01
1
\$\begingroup\$

I think the reason you are not getting the efficiencies you are hoping for are the intentions behind most DI frameworks and containers.

For the example of the input validation you are not doing DI. You are implementing the strategy pattern. That is you have moved the logic for specific types of validation out of your validation manager object. This is a great idea and gives you massive amounts of flexibility and functionality without one class getting too bloated.

The strategy pattern is one way to achieve inversion of control (IoC). Let's not forget that DI is also a form of IoC. So while what you are doing is IoC it is not DI.

To get the connivance you are looking for creating a builder that maybe took arrays of params or had specialized methods for common cases would be a good approach.

IoC is a very good way to write code and I think you are going down the correct path. I think you have just gotten side tracked by solutions for other problems.

Hope this helps.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.