Serban Alexandru Serban Alexandru - 1 month ago 6
PHP Question

Dependency Injection Container PHP

I've recently learned about the advantages of using Dependency Injection (DI) in my PHP application.

However, I'm still unsure how to create my container for the dependencies. Before, I use a container from a framework and I want to understand how is he doing things in back and reproduce it.

For example:

The container from Zend 2.I understand that the container make class dynamic, he doesn't have to know about them from begining, he checks if he already has that class in his registry and if he hasn't he check if that class exist and what parameters has inside constructor and put it in his own registry so next time could take it from there, practical is doing everything dynamic,and is completing his own registry, so we don't have to take care of nothing once we implement the container as he can give as any class we want even if we just make that class.

Also if I want to getInstance for A wich needs B and B needs C I understand that he doing this recursive and he goes and instantiate C then B and finally A.

So I understand the big picture and what is he supose to do but I am not so sure about how to implement it.

Answer

You may be better off using one of the existing Dependency Containers out there, such as PHP-DI or Pimple. However, if you are looking for a simpler solution, then I've implemented a Dependency Container as part of an article that I wrote here: http://software-architecture-php.blogspot.com/

Here is the code for the container

    class Container implements \DecoupledApp\Interfaces\Container\ContainerInterface 
{
    /**
     * This function resolves the constructor arguments and creates an object
     * @param string $dataType
     * @return mixed An object
     */
    private function createObject($dataType)
    {
        if(!class_exists($dataType)) {
            throw new \Exception("$dataType class does not exist");
        }
        $reflectionClass = new \ReflectionClass($dataType);
        $constructor = $reflectionClass->getConstructor();
        $args = null;
        $obj = null;
        if($constructor !== null)
        {
            $block = new \phpDocumentor\Reflection\DocBlock($constructor);

            $tags = $block->getTagsByName("param");
            if(count($tags) > 0)
            {
                $args = array();
            }
            foreach($tags as $tag)
            {
                //resolve constructor parameters
                $args[] = $this->resolve($tag->getType());
            }
        }
        if($args !== null)
        {
            $obj = $reflectionClass->newInstanceArgs($args);
        }
        else
        {
            $obj = $reflectionClass->newInstanceArgs();
        }

        return $obj;
    }

    /**
     * Resolves the properities that have a type that is registered with the Container. 
     * @param mixed $obj
     */
    private function resolveProperties(&$obj)
    {
        $reflectionClass = new \ReflectionClass(get_class($obj));
        $props = $reflectionClass->getProperties();
        foreach($props as $prop)
        {
            $block = new \phpDocumentor\Reflection\DocBlock($prop);

            //This assumes that there is only one "var" tag.
            //If there are more than one, then only the first one will be considered.
            $tags = $block->getTagsByName("var");
            if(isset($tags[0]))
            {
                $value = $this->resolve($tags[0]->getType());

                if($value !== null)
                {
                    if($prop->isPublic()) {
                        $prop->setValue($obj, $value);
                    } else {
                        $setter = "set".ucfirst($prop->name);
                        if($reflectionClass->hasMethod($setter)) {
                            $rmeth = $reflectionClass->getMethod($setter);
                            if($rmeth->isPublic()){
                                $rmeth->invoke($obj, $value);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * 
     * @param string $dataType
     * @return object|NULL If the $dataType is registered, the this function creates the corresponding object and returns it;
     * otherwise, this function returns null
     */
    public function resolve($dataType) 
    {
        $dataType = trim($dataType, "\\");
        $obj = null;
        if(isset($this->singletonRegistry[$dataType])) 
        {
            //TODO: check if the class exists
            $className = $this->singletonRegistry[$dataType];
            $obj = $className::getInstance();
        } 
        else if(isset($this->closureRegistry[$dataType]))
        {
            $obj = $this->closureRegistry[$dataType]();
        }
        else if(isset($this->typeRegistry[$dataType])) 
        {
            $obj = $this->createObject($this->typeRegistry[$dataType]);
        }

        if($obj !== null) 
        {
            //Now we need to resolve the object properties
            $this->resolveProperties($obj);
        }
        return $obj;
    }

    /**
     * @see \DecoupledApp\Interfaces\Container\ContainerInterface::make()
     */
    public function make($dataType)
    {
        $obj = $this->createObject($dataType);
        $this->resolveProperties($obj);
        return $obj;
    }

    /**
     *
     * @param Array $singletonRegistry
     * @param Array $typeRegistry
     * @param Array $closureRegistry
     */
    public function __construct($singletonRegistry, $typeRegistry, $closureRegistry) 
    {
        $this->singletonRegistry = $singletonRegistry;
        $this->typeRegistry = $typeRegistry;
        $this->closureRegistry = $closureRegistry;
    }

    /**
     * An array that stores the mappings of an interface to a concrete singleton class. 
     * The key/value pair corresond to the interface name/class name pair.
     * The interface and class names are all fully qualified (i.e., include the namespaces).
     * @var Array
     */
    private $singletonRegistry;

    /**
     * An array that stores the mappings of an interface to a concrete class. 
     * The key/value pair corresond to the interface name/class name pair.
     * The interface and class names are all fully qualified (i.e., include the namespaces).
     * @var Array
     */
    private $typeRegistry;

    /**
     * An array that stores the mappings of an interface to a closure that is used to create and return the concrete object.
     * The key/value pair corresond to the interface name/class name pair.
     * The interface and class names are all fully qualified (i.e., include the namespaces).
     * @var Array
     */
    private $closureRegistry;

}

The above code can be found here: https://github.com/abdulla16/decoupled-app (under the /Container folder)

You can register your dependencies as a singleton, as a type (every time a new object will be instantiated), or as a closure (the container will call the function that you register and that function is expected to return the instance).

For example,

$singletonRegistry = array();
$singletonRegistry["DecoupledApp\\Interfaces\\UnitOfWork\\UnitOfWorkInterface"] =
    "\\DecoupledApp\\UnitOfWork\\UnitOfWork";


$typeRegistry = array();
$typeRegistry["DecoupledApp\\Interfaces\\DataModel\\Entities\\UserInterface"] = 
    "\\DecoupledApp\\DataModel\\Entities\\User";

$closureRegistry = array();
$closureRegistry["DecoupledApp\\Interfaces\\DataModel\\Repositories\\UserRepositoryInterface"] = 
    function() {
        global $entityManager;
        return $entityManager->getRepository("\\DecoupledApp\\DataModel\\Entities\\User");
    };

$container = new \DecoupledApp\Container\Container($singletonRegistry, $typeRegistry, $closureRegistry);

This Container resolves properties of a class as well as the constructor parameters.

Comments