PsySl Service Locator Experiment

Posted on: Friday April 26, 2013

Yesterday, during a conversation with my coworkers on how Dependency Injection works with the Service Locator pattern, we were talking through what would happen with cyclical dependencies. Basically, what would happen if you told a Service locator to load a dependency for one class, and that class had a dependency for the class that was originally calling the Service Locator. This would cause an infinite loop, and in PHP, Maximum Memory Exceeded error pretty rapidly.

In PHP an example would be:

class A
{
    public $b;

    public function __construct(\PsySl\ServiceLocator $sl)
    {
        $this->b = $sl->resolve('B');
    }

    public function getSomething($llama)
    {
        return "llama = $llama";
    }
}

class B
{
    public $a;

    public function __construct(\PsySl\ServiceLocator $sl)
    {
        $this->a = $sl->resolve('A');
    }
}

$sl = new \PsySl\ServiceLocator();
$sl->register('A', 'A')
   ->register('B', 'B');

$a = new A($sl);

Class A attempts to resolve a dependency for class B, class B then tries to get a dependency for class A, and so on. There is a way to avoid this, turns out, if you're willing to make some concessions.

This is where my PsySl comes into play. It is basically a small experiment to see if I could deal with the cyclical dependency loop. Here's the meat of ServiceLocator class:

public function resolve($name)
{
    if (!empty(self::$openDeps[$name])) {
        $res = new StubResolver($name, $this);
        self::$neededResolutions[$name] = $res;

        return $res;
    }

    $resolvedDependencyKey = null;
    $trace = debug_backtrace();
    if (!empty($trace[1]) && !empty($trace[1]['class'])) {
        $resolvedDependencyKey = array_search($trace[1]['class'], self::$dependencies);

        if ($resolvedDependencyKey !== false) {
            self::$openDeps[$resolvedDependencyKey] = true;
        }
    }

    if (!isset(self::$dependencies[$name])) {
        throw new Exception("Unable to find dependency $name");
    }

    if (!isset(self::$resolvedDependencies[$name])) {
        self::$resolvedDependencies[$name] = new self::$dependencies[$name]($this);
    }

    if (!empty($resolvedDependencyKey)) {
        unset(self::$openDeps[$resolvedDependencyKey]);
    }

    return self::$resolvedDependencies[$name];
}

There's a lot going on in this method. First, it checks to see if it has any open dependencies (things that have called the resolution stack but have yet to be completely resolved). If it does, it creates a new StubResolver and sets the dependency it is a mock for. Next, it checks the stack trace to see if the calling class is one of the things it is registered to resolve (and really, it should probably check to see if it is the construct method, since otherwise it wouldn't cause a loop). If this is the case, it appends it to the list of open dependencies and continues on. The last bit is it tries to resolve the original dependency it was trying to resolve, closes its open dependency, and continues along. Additionally it keeps a copy of the dependency it created in a static variable so it StubResolver can later resolve its dependency without entering an infinite loop. (Which consequently means the dependencies have to be singletons, a problem I'm not sure if it is possible to get around).

That's about where the magic happens. In our new A($sl) example, A tries to get a copy of B, and in the process registers A as an open dependency. B then tries to resolve A, and is returned a StubResolver instance for its "A" dependency. B finishes resolving, and everyone is happy.

But wait, now B has a StubResolver in place of its $a parameter. How does it fully resolve to A? Well, StubResolver has a magic __call method which does the following:

public function __call($method, $params)
{
    if (!isset($this->_dependency)) {
        $this->setDependency($this->_serviceLocator->resolve($this->getDependencyName()));
    }

    return call_user_func_array(array($this->_dependency, $method), $params);
}

So, as you can see, there is a lazy load of the dependency the first time you call a method on it. You would need to implement __get and __set which will also do the same, and forward those along to the dependency as well.

All of this is incredibly impractical and breaks all kinds of conventions, but it was a fun exercise to dig a bit into the associated patterns and how php can be manipulated for evil (or good). You can view the full source over here: https://github.com/cthos/PsySl