loading...
Cover image for Building a PHP Framework: Part 7 - The Container

Building a PHP Framework: Part 7 - The Container

mattsparks profile image Matt Sparks ・4 min read

Orginially Posted on DevelopmentMatt.com

Part 6 began our discussion of PHP containers. Today, I'll be going into greater detail of the subject and, along with that, I'll run down the work done so far on the Analyze container.

A huge debt of gratitude goes how to the folks behind the PHP League Container and others. I've learned a ton studying their code.

A Quick Recap

What are containers and why do we use them? A container is both a registry composed of objects and a mechanism for retrieving them. It's the library and the librarian, so to speak. Containers provide developers a tool to more easily manage dependencies.

The Analyze Container

Work has begun on the container that will be used in the Analyze framework and, as of this writing, the foundation is essentially complete. Keep in mind that it's not finished, but it is far enough along to discuss it.

All of the code for the container can be found on Github.

A Simple Example

Currently, there are two ways to register an object with the container. The first is by using a factory. A factory uses a Closure to build the object upon retrieval.

$container = new Container;

$container->addFactory('Carbon', function () {
    return new \Carbon\Carbon;
});

$carbon = $container->get('Carbon');

var_dump($carbon->year) // 2018

The second option is to register a class using it's fully qualified name.

$container = new Container;

$container->addClass('Carbon', '\Carbon\Carbon');

$carbon = $container->get('Carbon');

var_dump($carbon->year) // 2018

Both methods take an alias as their first parameter.

How The Container Works

At its most basic core, the container is simply an object with a parameter – an array – that holds object definitions. When we ask for a dependency (get('Carbon')), the container looks for it in that array. If it's found, the object is built and returned. If not, an exception is thrown. Specifically, a NotFoundException exception.

<?php 

[...]

class Container implements ContainerInterface
{

    /**
     * Definitions
     * @var array
     */
    private $definitions = [];

    [...]

    /**
     * Get
     *
     * Returns the passed alias.
     *
     * @param  string $id
     * @return mixed
     * @throws NotFoundException
     */
    public function get($id)
    {
        if (!$this->has($id)) {
            throw new NotFoundException(sprintf('%s is not defined.', $id));
        }

        $definition = $this->definitions[$id];
        $definition->addArguments($this->arguments);

        return $definition->build();
    }

    [...]

}

Definitions

Definitions are the most important aspect of the container. They define how to build objects and without that functionality there's no use having a container. Each definition must implement the DefinitionInterface.

interface DefinitionInterface
{
    /**
     * Build
     *
     * @return object
     */
    public function build();
}

That means, each definition must have a build method that returns an object.

Each definition should, but aren't technically required to, extend the AbstractDefinition class.

abstract class AbstractDefinition implements DefinitionInterface
{
    /**
     * Arguments
     * @var array
     */
    public $arguments = [];

    /**
     * Add Arguments
     *
     * @param array $args
     */
    public function addArguments(array $args) : void
    {
        $this->arguments = $args;
    }

    /**
     * Has Arguments
     *
     * @return bool
     */
    public function hasArguments() : bool
    {
        return count($this->arguments) > 0;
    }
}

This class provides properties and methods that are used across definitions.

class FactoryDefinition extends AbstractDefinition
{
    /**
     * Callback
     * @var Closure
     */
    private $callback;

    /**
     * Constructor
     *
     * @param Closure $callback
     */
    public function __construct(Closure $callback)
    {
        $this->callback = $callback;
    }

    /**
     * Build
     *
     * @return object
     */
    public function build()
    {
        return call_user_func_array($this->callback, $this->arguments);
    }
}

class ClassDefinition extends AbstractDefinition
{
    /**
     * Concrete
     * @var string
     */
    private $concrete;

    /**
     * Constructor
     *
     * @param string $concrete
     */
    public function __construct(string $concrete)
    {
        $this->concrete = $concrete;
    }

    /**
     * Build
     *
     * @return object
     */
    public function build()
    {
        if ($this->hasArguments()) {
            $reflection = new ReflectionClass($this->concrete);

            return $reflection->newInstanceArgs($this->arguments);
        }

        return new $this->concrete;
    }
}

As you can see, both types of definition describe how to build an object.

Arguments

I'm sure you've noticed in the code above that arguments can be used when building an object. For example, let's say you can to pass a date and time when building a Carbon instance. Here's what that would look like:

$container->withArguments(['2010-05-16 22:40:10'])->get('Carbon');

What's Left to do

There's still quite a bit of work to be done on the container, but I'm pretty happy with where things are. The most pressing features left are implementing a SetterDefinition and, related to that, the ability to call methods. I'll also be using reflection to automatically inject dependencies. I'm not entirely sure how that's going to look, but I'm excited to get started on it.

Feedback

I'd love to hear from you. If you have suggestions or whatever else, hit me up on my Twitter machine.

One last thing, be sure to checkout my newsletter! Each week I'll send you a great email filled with updates, great links, tips & tricks, and other non-dev randomness. If you're interested you can sign up by following this link.

Discussion

pic
Editor guide