Using dependency injectors to build "just-in-time" object-oriented code evaluation

Note: if you haven't yet watched Star Wars, especially The Empire Strikes Back, there are spoilers below. But go watch it! I'll wait. Seriously: it's a long film, but it's worth it. While you're watching it, I'll watch it as well: how's that?

Frameworks use dependency injection as a software pattern to make their code more testable: if you've got some code that's expecting a real-live request from a real-live webserver, but you can inject a mock request into it, then unit tests can be written against that code. But dependency injection is often difficult to get right, and difficult to sustain: it's all too easy to start hiding dependencies inside, later on.

Enter a dependency injection container: Symfony has a fully-featured one; more lightweight options exist, like Pimple.

What if I don't have any dependencies?

So what, you might ask? Well, DI containers are basically a way of assembling code logic, so that the logic can be executed at a later date. They're almost like a simplistic virtual PHP environment, within your PHP environment. In fact, a bit like eval(), only with a limited logic that's much more robust and maintainable.

Can we use a DI container to rebuild eval()? Do we have the technology? Can we can make it better than it was? Better... stronger... faster? Well, yes!

The most important reveal in the Star Wars universe, reimagined in code

Imagine watching Star Wars IV: A New Hope the first time round (as it was the first one released) and thinking, hey, Luke's an orphan. Maybe Obi Wan Kenobi was secretly his dad! It makes a lot of sense, especially if you see the films as a kind of sci-fi fairytale, rather than a straight story. Anyway, if you ever thought that, then The Empire Strikes Back would have been a real eye-opener for you.

In honour of such a delusion, here's some code that encapsulates it in PHP:

<?php
 
// father-of-luke.php
 
class Person
{
    public function __construct($name)
    {
        $this->name = $name;
    }
 
    public function setFather(Person $father)
    {
        if ($father->name == "Darth Vader"
            && $this->name == "Luke Skywalker")
        {
            print "No! That's not true! That's impossible!\n";
        }
        else
        {
            print "Hi, Dad!\n";
        }
    }
}
 
$obiWan = new Person("Obi Wan Kenobi");
$darth  = new Person("Darth Vader");
$luke   = new Person("Luke Skywalker");
 
$luke->setFather((isset($argv[1]) && $argv[1] == "darth") ? $darth : $obiWan);

The above is a script that you can run, to decide which of the two Jedis from A New Hope was actually Luke's father. If you run it like this:

$ php father-of-luke.php
Hi, Dad!

it assumes Obi Wan is Luke's father, but if you run it like this:

$ php father-of-luke.php darth
No! That's not true! That's impossible!

then it—spoiler!—gives you a quote from the film. That's all it does.

Using Symfony's DI container to build this code, in code

Let's see if we can rebuild the procedural bits of the above, using Symfony's DI container. We're going to use composer to bring that in: if you haven't used it yet, I can really recommend it.

To configure composer actions, you create a composer.json file:

{
    "require": {
        "symfony/dependency-injection": "2.4.*"
    }
}

Put this in the same directory as father-of-luke.php, then run: composer update. It should fetch just the one Symfony component, and at the same time create a vendor/autoload.php that you can include to ensure your PHP scripts can autoload any classes it finds.

We'll do that very thing. Create this new version of our script, now using a DI container:

<?php
 
// father-of-luke-container.php
 
class Person
{
    public function __construct($name)
    {
        $this->name = $name;
    }
 
    public function setFather(Person $father)
    {
        if ($father->name == "Darth Vader"
            && $this->name == "Luke Skywalker")
        {
            print "No! That's not true! That's impossible!\n";
        }
        else
        {
            print "Hi, Dad!\n";
        }
    }
}
 
// Include composer's autoload, to let PHP find the Symfony classes.
require_once "vendor/autoload.php";
// Bring in the DI container and the reference object (for building references).
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
$sc = new DependencyInjection\ContainerBuilder();
 
$sc->register('obiWan', 'Person')->setArguments(array("Obi Wan Kenobi"));
$sc->register('darth',  'Person')->setArguments(array("Darth Vader"));
$sc->register('luke',   'Person')->setArguments(array("Luke Skywalker"))
    ->addMethodCall('setFather', array(new Reference(
        (isset($argv[1]) && $argv[1] == "darth") ? "darth" : "obiWan"
    )));
 
$luke = $sc->get('luke');

The end result is the same, and it looks like basically we've had to write a lot more code to get there. Not impressive, is it?

Well, that's a good point. But notice now that we never actually instantiate the darth/obiWan object we don't use. We register the idea of "darth" and "obiWan" references, with a classname (as a string, not as a class) and the future argument(s) to pass to each; then we register the idea of a "luke" object, which immediately after __construct()-ion has a method called on it, with a future argument that depends on the command line parameter: but it's only at the point of calling ->get() on the DI container that the required objects in the current dependency tree—and no others—are instantiated.

This is the bit that I claim is like a sanitized version of eval(): but instead of building code to be evaluated by concatenating strings together, you build it using objects, and method calls on those objects. In both cases, the methodology means that very little actually happens until that structure is evaluated (or even eval()-uated.)

What happens if we autoload everything?

The really exciting bit is when we remove Person from the executable file, and put it in its own classfile. We can train composer's autoload.php to find it for us, if we make sure we use the PSR-0 naming scheme to match our subfolders to our object namespaces.

Move the Person object into a file src/Family/Person, and prefix the file with a namespace declaration as follows:

<?php
 
// src/Family/Person.php
 
namespace Family;
 
class Person
{
    public function __construct($name)
    {
        $this->name = $name;
    }
 
    public function setFather(Person $father)
    {
        if ($father->name == "Darth Vader"
            && $this->name == "Luke Skywalker")
        {
            print "No! That's not true! That's impossible!\n";
        }
        else
        {
            print "Hi, Dad!\n";
        }
    }
}

We can then tell composer to look for namespaces beginning Family/, in PSR-0-standard subfolders, starting at src/, by changing the composer.json file to look like this:

{
    "require": {
        "symfony/dependency-injection": "2.4.*"
    },
    "autoload": {
        "psr-0": { "Family": "src/" }
    }
}

Create a copy of the script called just-container.php and remove all traces of objects from it:

<?php
 
// just-container.php
 
require_once "vendor/autoload.php";
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference ;
$sc = new DependencyInjection\ContainerBuilder();
 
$sc->register('obiWan', 'Family\Person')->setArguments(array("Obi Wan Kenobi"));
$sc->register('darth',  'Family\Person')->setArguments(array("Darth Vader"));
$sc->register('luke',   'Family\Person')->setArguments(array("Luke Skywalker"))
    ->addMethodCall('setFather', array(new Reference(
        (isset($argv[1]) && $argv[1] == "darth") ? "darth" : "obiWan"
    )));
 
$luke = $sc->get('luke');

Thanks to the just-in-time nature of the use statement, Family\Person isn't even registered as a class until ->get() is called, never mind an object! Huge class structures can be safely hidden from your main script and only introduced just in time, right at the point that you evaluate your dependency tree.

Performance improvements in action

Let's simulate performance overhead with some sleep() statements. Imagine that Obi Wan is no longer a Family\Person object: he's a Family\Goodie object. Here's what one of those looks like, saved to src/Family/Goodie.php:

<?php
 
// src/Family/Goodie.php
 
namespace Family;
 
class Goodie extends Person
{
    public function __construct($name)
    {
        print "$name is a good guy, but he takes 5 seconds to start.\n";
        sleep(5);
        parent::__construct($name);
    }
}
 
print "Autoloading Goodie: waiting 2 seconds.\n";
sleep(2);

We use two delays: one to simulate PHP having to parse the file and register what might instead be a huge and complex class; and the other to simulate a resource-intensive constructor function on the object. We'll also need to change the Family\Person string in just-container.php to read Family\Goodie, just for the obiWan DI registration.

What happens now, if we run our script so that Goodie.php is both parsed and then an object created from its class?

$ time php just-container.php
... 7.048 total

Just over seven seconds, as we'd expect. But if luke's dependency tree does not include Family\Goodie?

$ time php just-container.php darth
... 0.034 total

A fraction of a second. Goodie.php has clearly been completely ignored by the DI container, as we would expect and most definitely desire.

Summary

Dependency injection is an important code pattern anyway, but here I've demonstrated (with my tongue slightly in my cheek) that a DI container can be used to assemble and then evaluate semi-arbitrary code: and what we lose in arbitrariness, we gain in strength and transparency. Am I saying you should use a DI container to do this? Well, maybe: it really depends on your use case. Am I saying you can, if you really want? Absolutely!

Most importantly, this demonstrates what's so good about a DI container: that its just-in-time loading of classes and instantiating of objects can really improve the handling of potentially resource-hungry PHP classes and objects. Better, stronger and—as I promised—faster.

Also, I've totally given away the big reveal in the original Star Wars trilogy. My next tutorial will be all about Dumbledore and Snape: I bet you can't wait.