Tuesday, September 19, 2017

Reading Source code for Pimple 1.x

I have huge respect for Fabian Potencier, the man is a code machine, and in addition to his work in open source, you have to admire his commitment to educating other devs (check out his "create your own framework" series).

Recently I was researching Dependency Injection and came across Pimple, Silex's (the microframework) DI container. There's a helpful note on the frontpage that tells us that reading the source for Pimple 1.x is a great way of learning how to write a simple DI container.
So I read it. Here are my notes.

Preamble - Dependency Injection


So why worry about DI generally? Well, good question, and one covered quite well in the Wikipedia article I've linked to above. For completeness, though (and for my own peace of mind) I'll give a quick take on it here.
Dependency Injection describes a pattern or technique that, if implemented, frees up your objects from having to worry about how their dependencies are created, and indeed, from creating them themselves.

Compare

Class A
{
    private $databaseConnection;
    public function __construct()
    {
         $this->databaseConnection = new \DatabaseConnection("server", "username", "password", "catalogue");
     }
    /** snip **/
}


In this first example (whatever other issues it has) our class A is tightly coupled to the \DatabaseConnection class. Our class can only ever use this kind of db connection (rather than say some specialized sub-class, or a mock while testing).
If the \DatabaseConnection class ever changes, say it needs a new parameter when it's instantiated, we'll need to change our class's own constructor as well (SRP, hi!).

The usual way of sorting this out is to pass through any objects that our own class might rely on ...

Class B
{
    private $databaseConnection;
    public function __construct(\DBConnectionInterface $databaseConnection)
    {
        $this->databaseConnection = $databaseConnection;
    }
    /** snip **/
}
Notice now we've loosened up our class's relationship to the DatabaseConnection class - when newing up our own class, we now simply pass through a DatabaseConnection object, and because we're coding to an interface, we can merrily use our $this->databaseConnection regardless of what actual class is passed through to us.

This seems to just push back our problem though, right? Isn't the code that goes ahead and creates the instance of our class B now responsible for creating the objects it's dependent on. And What if we create instances of B in more than one place? Surely the virtue of having a constructor is that we only really need to describe its set up in a single place? This is true, which is why the DI pattern exists.

Central to it is the idea of an injector, or--more commonly--a container (see the wikipedia article for the rest of the synonyms). The container represents a place where the creation of objects can be centralized. Rather than have our code, say, create new database connections in several places, we centralize the logic for creating databases in our container. If how we create a database connection changes, we only need to change that in a single place.
Not only that, if any of the objects in the container have dependencies themselves, the container will be able to manage them too - long chains of dependencies are resolved inside the container.

And this is what Pimple does - it's a DI container. We tell it how to create things like our Database connection, and then when we need one in our code, we just ask the container to give us one.

In our example, we could create a new instance of class B with our container doing something like ...

$b = new B($container['databaseConnection']);

or, in some cases it might be appropriate to have the container create our instances of our B class itself ...

$b = $container['B'];


The Code


There isn't much to Pimple 1.1. Just over 200 lines of code with comments and whitespace. It does a lot with a little though.
I'll be giving line numbers from here - so feel free to follow along.

ArrayAccess

First things, it implements (line 33) the ArrayAccess interface. If the name doesn't give it away, this interface lets our objects act as arrays, i.e. calling code is able to interact with the object using the square bracket syntax.

This let's us both put stuff in the container like this

$c['key'] = $value; //C1

and read stuff from the container like this

$value = $['key']; //C2

I'd be interested to know why Fabpot went this way. My guess is that the assignment side is made nice and clean.
Consider an alternative - putting something into the container with some kind of setter

$c->setValue('valueName', $value); //C3

I don't see this being a showstopper, but it's certainly not as concise as C1 above.
That's just a guess, though.

A lot of the functionality of the class is simply implementing the ArrayAccess interface (i.e. providing implementations of offsetExists, offsetGet, offsetSet, offsetUnset).

Defining parameters

I'm following the sections in the documentation here, looking at what kinds of things the service container can store, and seeing how this is achieved.
First off are parameters, these are just straightforward values. There's not much to be said about these - most of the work in the code is done to figure out when we're not working with parameters.

With parameters, the container is simply acting as an abstraction over the internal store (line 35).
It's important that the container be able to work with simple values without too much overhead because simple values are also part of the dependencies needed to create objects (in our DB connection example, things like the username and password may be considered parameters).

Having all our dependencies accessible through a common interface seems like good design.
This point about common interfaces will become clearer when we see the recursive nature of service definition next.

Defining services

Services are the heart of the DI pattern. Insofar as Pimple defines them, they're "Object[s] that [do] something as part of a larger system." Consider the case I described in the preamble - we want to create a DatabaseConnection service. This is achieved by passing an anonymous function to the container that knows how to create, and will return, the object type.

Our example might be something like

$containerInstance['DBConnection'] = function ($c) {
   return new \DatabaseConnection($c['server'], $c['username'], $c['password'], $c['catalogue']);
};

Pretty straightforward -- note, though, the argument $c that's passed to the function, that's an instance of the container itself! This is what enables the container to resolve any dependencies that a service might require (assuming that the dependencies are actually defined in the container - if not, expect errors).

The magic here comes in line 81, in the offsetGet() method, where we try pull the service out. We check whether the thing we're trying to pull out is an Object, and if it is, whether it's invokable. Recall that a php anonymous function just is an instance of a particular class (http://php.net/manual/en/class.closure.php), which is why that check works as it does.

If an invokable class is found, Pimple will call it and pass itself as as argument (for further dependency resolution, if required).

Shared services

Shared services act like a singleton (the Laravel container uses this terminology explicitly) - instead of returning a brand new object every time the service is requested, it'll create the object once, and return the same instance on subsequent calls.

You do this by adding running your function through the container's share() method before adding it, like so:

$containerInstance['DBConnection'] = $containerInstance->share(function ($c) {
   return new \DatabaseConnection($c['server'], $c['username'], $c['password'], $c['catalogue']);
});

This is where things start getting slightly more interesting - the share() function (lines 116-131) does the following:
First checks that what we've passed through is actually invokable (line 118, same logic as described above), throwing an exception if it isn't.

If it is, then it wraps it in a new anonymous function which makes use of a static variable to store the shared service object (i.e. whatever the anonymous function we pass in returns). When you use the static keyword inside a function, the variable declared as static does not lose its value after it's finished executing. This is dangerous, but really powerful (read more here).

Essentially, what the share() method does is check whether it has ever been run before (i.e. is the static variable null) and if it hasn't, it runs the function we've passed it and stores the result.
This is a fairly standard memoization pattern, less often seen in PHP than in the JS world, in my experience.

Protecting Parameters

You might've noticed a problem here. What if we want to store an anonymous function as a parameter? That is, what if we want to be able to retrieve a function from our container without the container running it? As it stands, whenever we drop an invokable into the container, it's going to be run with the container itself as an argument.
To avoid that, Pimple provides the protect() method (lines 142-151) - it's used the same way as the share() method and does the same kind of thing - wraps our function in another function, except, in this case, all the wrapping function does is return the wrapped function, rather than execute it.

Note that we can pull out functions from the container without them being executed by using the raw() method (lines 162-169).

Extending Services

The last little piece of functionality I want to cover is the mechanism used for extending services after creation. The idea here is that we might want to take the results / output of a service and modify it in some way before it's resolved/returned. This is achieved by, once again, wrapping functions within functions. In this case, we're taking both the original callable function and our extension and wrapping them up in a third.

Let's take the documentation's example of modifying a mail service:

$container['mail'] = function ($c) {
    return new \Zend_Mail();
};

$container['mail'] = $container->extend('mail', function($mail, $c) {
    $mail->setFrom($c['mail.default_from']);
    return $mail;
});

See that the function doing the extension takes two parameters, the first ($mail) being the original value of mail in the container, and $c which, as always, is the container instance itself.

Most importantly, how is this accomplished in the extend() method (lines 184-203)?
The meat of it all, after a few tests, lives on lines 200-202. Here, what's happening is that Pimple is taking the existing service and passing it and the extension function that we've defined to a new anonymous function via the "use" keyword (i.e. the new anonymous function inherits them from its parent's scope).
The cool thing about this is that extensions can be extended in this way indefinitely through a series of nested calls.

Next Steps


I was planning on writing my own DI container based on Pimple (as part of a larger project), if I do, I'll post the code up here too.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.