Dependency Injection & Reflection

Overview
With the new features of PHP 5.3 we’ve never had it so good. Dependency injection is a well known concept, so why another?
Author
Rhodri Pugh
Date
29 November 2010

 

Download the code from Github : Current release 1.4.6

Dependency injection and reflection library

PHP development really feels like it's grown up in recent years, and with all the great new features landing in 5.3 we've never had it so good. Dependency injection is a well known concept, and there are currently a plethora of DI containers available from one of the interweb's many pipes. These include Symfony and Crafty, while the Zend Framework also has its own DI container.

So why another?

One of my personal goals as a developer is to make things so simple, even I can understand them. But most of the DI containers I've come across require too much configuration, and usually via XML - yuck! So how is ours different?

This article assumes you're familiar with the concept of dependency injection, if you're not already using this technique then you should read some docs and come right back!

Creating an injector

The one requirement the library has is that Addendum is loaded.

include '../boxuk-di/lib/bootstrap.php';
$helper = new BoxUKInjectHelper();
$injector = $helper->getInjector();

The easiest way to get your injector set up and ready to go is to use the Helper class. This will create you a standard injector with all the defaults. You're then good to go...

$class = $injector->getClass( 'MyClass' );

Type hinting dependencies

The injector will use type hinting to determine your class dependencies, so make sure to specify them in your constructor. If you don't, no injection for you sorry...

public function __construct( SomeClass $classOne ) { ... }

Scope, and annotations

Using what we've seen so far, every time we ask for a class we're going to get a brand new one back. If, for example, you have objects that you only want one of you need to specify their scope using an annotation.

/**
* @ScopeSingleton
*/
class MyClass { ... }

This tells the injector that there should only be one of these at a time, so the second call, or the second class to specify this as their dependency will get the same one.

Another scope available is session scope, which stores objects in the session and returns them on subsequent requests.

Creating more objects...scope, and annotations

So - you might be thinking - do I need to pass this injector around now, to every object when I want to create something? The answer is - no. When the injector is created it is then available as a dependency like other classes, so if you need to use it in one of your classes...

public function __construct( BoxUKInjectInjector $injector ) { ... }

Just ask for it!

Primitives

What about primitives? Your objects may currently take in arrays and strings to their constructors, what about these? Primitives aren't really dependencies, they're usually configuration. So if your class takes both object dependencies and primitives to its constructor then the primitives will need to be refactored to a later initialisation phase. For example...

class MyClass {
public function __construct( AnotherClass $class ) { ... }
public function init( $port, $params ) { ... }
}

There is nothing special about the init() function, it is just an arbitrary method name. Also settings like this can often be abstracted to classes (maybe a single Settings object) which can then be injected.

The rule should be objects are dependencies, everything else is configuration.

You can also annotate methods and properties to be injected...

/**
* @InjectProperty
* @var AnotherClass
*/
private $anotherClass;

/**
* @InjectMethod
*/
public function setSomeClass( SomeClass $someClass ) {
$this->someClass = $someClass;
}

Property injection even handles setting private members so you don't have to open your class up, and method injection has no constraint over the name of the method or the number of dependencies you ask for.

Isn't reflection slooooooooow?

If you've ever looked into reflection before you'll probably know that it's not the fastest part of PHP. It's not sloooooooow as such... but for high performance web apps it just doesn't cut the mustard. Which is why the reflection and annotation part of the library is split out into its own BoxUKReflect namespace - and includes a whole bunch of caching support to make it nice and fast.

This means you can get all the benefits of using these PHP features, and none of the headaches of waiting for them to happen. Bonza!

The easiest way to create your own reflector is to use the same Helper object we used to create our injector earlier, just call getReflector() instead. You'll need to pass in a Config object if you want to set up caching, full info is in the main documenation.

Testing

The injector works great when it comes to unit testing. At Box UK we often sub-class the injector to provide an application specific test injector which will know how to provide useful defaults for unit tests (where customized canned objects are not provided).

$object = $testInjector->getClass( 'SomeClass', array(
'DependencyClass' => $dependency
));

More stuff

So that's a brief overview of what the library provides, but there's heaps more stuff like how to tweak method injection, binding to interfaces, creating your own session handler for session scoped objects, etc. For full details check the main documentation.

Conclusion

This library provides a simple, lightweight, and fast way to architect and test your PHP application. It's designed to be easily extendable and configurable, so hopefully you'll find it useful in freeing yourself from dependency/singleton hell.

Grab the code from https://github.com/boxuk/boxuk-di and enjoy!

Comments

Add a comment

  • Julio

    22 Dec 2010 10:23
    Any php accelerator will remove comments which means that you have to decide between speed vs light code + flexibility. I'm betting on speed ;)
    Check stubbles php framework. The developer is a dependency injection freak that hates singletons :)). I like both but speed beats them anytime! :)
    PHP is not mature enough (yet) to handle DI. Maybe in 6.0 :D
  • Sebastian

    21 Jan 2011 21:48
    Hey, thank you for this nice DI framework! For my applications maintainability is a bigger concern than speed, so I really like what you have done here.

    The one feature, that is missing for me is the ability to bind implementations to interfaces, mainly for testing purpose. You say one simply has to "..subclass the injector..", but as I understand your sources, I also have to extend the Helper and that didn't work for me for some reason.

    It would be very nice, if you could provide some sample code with a "subclassed injector", as a simple testing scenario.

    best regards
    Sebastian
  • Rhodri Pugh

    4 Feb 2011 16:29 Box UK Staff

    Hi Sebastian,

    Thanks for the comments. There is support for binding to interfaces but that's only mentioned in the main documentation. When I mentioned subclassing the injector I meant this kind of thing...

    class TestInjector extends BoxUKInjectStandardInjector {
        public function getTestClass( $name, $dependencies=array() ) {
            foreach ( $dependencies as $class => $object ) {
                $this->getScope( 'singleton' )
                     ->set( $class, $object );
            }
            return $this->getNewClass( $name );
        }
    }
    

     


    Where we use the singleton scope to provide implementations for our test cases. Example usage being...

    $myImpl = new MyTestImplementation();
    $testCase = $testInjector->getTestClass( 'SomeClass', array( 'MyInterface' => $myImpl ) );
    

     


    You could also automatically provide defaults for $dependencies if people don't specify their own implementations.

    Hope that answers your question.

    rod.

Add your comment

This will not be shown
If provided, we'll link to this from your name