peakphp/framework

View on GitHub
src/Di/Container.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

declare(strict_types=1);

namespace Peak\Di;

use Peak\Di\Binding\Factory;
use Peak\Di\Binding\Prototype;
use Peak\Di\Binding\Singleton;
use Peak\Di\Exception\ClassDefinitionNotFoundException;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

use function array_search;
use function call_user_func_array;
use function class_implements;
use function get_class;
use function is_array;

class Container implements ContainerInterface
{
    /**
     * Container object instances collection
     * @var array
     */
    protected $instances = [];

    /**
     * Classes namespace alias
     * @var array
     */
    protected $aliases = [];

    /**
     * Container object interfaces collection
     * @var array
     */
    protected $interfaces = [];

    /**
     * Class instance creator
     * @var \Peak\Di\ClassInstantiator
     */
    protected $instantiator;

    /**
     * Container object instances collection
     * @var \Peak\Di\ClassResolver
     */
    protected $resolver;

    /**
     * Class definitions resolver
     * @var BindingResolver
     */
    protected $binding_resolver;

    /**
     * Allow container to resolve automatically for your object needed
     * @var bool
     */
    protected $auto_wiring = true;

    /**
     * Container configuration definitions
     * @var array
     */
    protected $definitions = [];

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->instantiator = new ClassInstantiator();
        $this->resolver = new ClassResolver();
        $this->binding_resolver = new BindingResolver();
    }

    /**
     * Instantiate a class
     *
     * The generated instance is not stored, but may use stored
     * instance(s) as dependency when needed
     *
     * @param string $class
     * @param array $args
     * @param null $explicit
     * @return mixed|object
     * @throws ClassDefinitionNotFoundException
     * @throws Exception\AmbiguousResolutionException
     * @throws Exception\InterfaceNotFoundException
     * @throws \ReflectionException
     */
    public function create(string $class, $args = [], $explicit = null)
    {
        // check first for definition even if auto wiring is on
        $def = $this->getDefinition($class);
        if ($def === null && !$this->auto_wiring) {
            throw new ClassDefinitionNotFoundException($class);
        } elseif ($def !== null) {
            return $this->binding_resolver->resolve(
                $this->getDefinition($class),
                $this,
                $args,
                $explicit
            );
        }

        // process class dependencies with reflection
        $args = $this->resolver->resolve($class, $this, $args, $explicit);

        // instantiate class with resolved dependencies and args if apply
        return $this->instantiator->instantiate($class, $args);
    }

    /**
     * Similar to instantiate(), it call a method on specified object
     *
     * @param array $callback
     * @param array $args
     * @param null $explicit
     * @return mixed
     * @throws ClassDefinitionNotFoundException
     * @throws Exception\AmbiguousResolutionException
     * @throws Exception\InterfaceNotFoundException
     * @throws \ReflectionException
     */
    public function call(array $callback, array $args = [], $explicit = null)
    {
        // process class dependencies
        $args = $this->resolver->resolve($callback, $this, $args, $explicit);

        return call_user_func_array($callback, $args);
    }

    /**
     * Same as create() but also store the created object before returning it
     *
     * @param string $class
     * @param array $args
     * @param null $explicit
     * @return mixed|object
     * @throws ClassDefinitionNotFoundException
     * @throws Exception\AmbiguousResolutionException
     * @throws Exception\InterfaceNotFoundException
     * @throws \ReflectionException
     */
    public function createAndStore(string $class, array $args = [], $explicit = null)
    {
        $object = $this->create($class, $args, $explicit);
        $this->set($object);
        return $object;
    }

    /**
     * Resolve a stored definition
     *
     * @param string $definition
     * @param array $args
     * @return mixed
     * @throws ClassDefinitionNotFoundException
     */
    public function resolve(string $definition, array $args = [])
    {
        $def = $this->getDefinition($definition);
        if ($def === null) {
            throw new ClassDefinitionNotFoundException($definition);
        }

        return $this->binding_resolver->resolve($def, $this, $args);
    }

    /**
     * Has object instance
     *
     * @param string $id
     * @return bool
     */
    public function has($id)
    {
        return isset($this->instances[$id]);
    }

    /**
     * Get an instance if exists, otherwise try to create it return null
     *
     * @param string $id
     * @return mixed|object
     * @throws ClassDefinitionNotFoundException
     * @throws Exception\AmbiguousResolutionException
     * @throws Exception\InterfaceNotFoundException
     * @throws \ReflectionException
     */
    public function get($id)
    {
        if ($this->has($id)) {
            return $this->instances[$id];
        } elseif ($this->hasAlias($id) && $this->has($this->aliases[$id])) {
            return $this->instances[$this->aliases[$id]];
        }

        return $this->create($id);
    }

    /**
     * Set an object instance. Chainable
     *
     * @param  object $object
     * @param  string|null $alias
     * @return Container
     */
    public function set(object $object, string $alias = null)
    {
        $class = get_class($object);
        $this->instances[$class] = $object;

        if (isset($alias)) {
            $this->addAlias($alias, $class);
        }

        $interfaces = class_implements($object);
        if (is_array($interfaces)) {
            foreach ($interfaces as $i) {
                $this->addInterface($i, $class);
            }
        }

        return $this;
    }

    /**
     * Delete an instance if exists.
     *
     * @param  string $id
     * @return Container
     */
    public function delete($id)
    {
        if ($this->has($id)) {
            //remove instance
            unset($this->instances[$id]);

            //remove interface reference if exists
            foreach ($this->interfaces as $int => $classes) {
                $key = array_search($id, $classes);
                if ($key !== false) {
                    unset($classes[$key]);
                    $this->interfaces[$int] = $classes;
                }
            }

            //remove instance from singleton binding
            if ($this->hasDefinition($id)) {
                $definition = $this->getDefinition($id);
                if ($definition instanceof Singleton) {
                    $definition->deleteStoredInstance();
                }
            }
        }
        return $this;
    }

    /**
     * Add a class alias
     *
     * @param  string $name
     * @param  string $className
     * @return $this
     */
    public function addAlias(string $name, string $className)
    {
        $this->aliases[$name] = $className;
        return $this;
    }

    /**
     * Has an alias
     *
     * @param  string $name
     * @return boolean
     */
    public function hasAlias(string $name)
    {
        return isset($this->aliases[$name]);
    }

    /**
     * Add container itself
     *
     * @return $this
     */
    public function addItself()
    {
        return $this->set($this);
    }

    /**
     * Get all stored instances
     *
     * @return array
     */
    public function getInstances()
    {
        return $this->instances;
    }

    /**
     * Catalogue also class interface when using add
     *
     * @param string $name
     * @param string $class
     */
    protected function addInterface(string $name, string $class)
    {
        if (!$this->hasInterface($name)) {
            $this->interfaces[$name] = $class;
            return;
        }
        $interfaces = $this->getInterface($name);

        if (!is_array($interfaces)) {
            $interfaces = [$interfaces];
        }

        if (!in_array($class, $interfaces)) {
            $interfaces[] = $class;
            $this->interfaces[$name] = $interfaces;
        }
    }

    /**
     * Has an interface
     *
     * @param  string $name
     * @return bool
     */
    public function hasInterface(string $name)
    {
        return isset($this->interfaces[$name]);
    }

    /**
     * Get an interface
     *
     * @param string $name
     * @return mixed|null
     */
    public function getInterface(string $name)
    {
        if ($this->hasInterface($name)) {
            return $this->interfaces[$name];
        }
        return null;
    }

    /**
     * Get all stored interfaces
     *
     * @return array
     */
    public function getInterfaces()
    {
        return $this->interfaces;
    }

    /**
     * Set class definition
     *
     * @param string $name
     * @param mixed $definition
     * @return $this
     */
    public function setDefinition(string $name, $definition)
    {
        $this->definitions[$name] = $definition;
        return $this;
    }

    /**
     * Set definitions. Use definitions when autowiring is off
     *
     * @param  array $definitions
     * @return $this
     */
    public function setDefinitions(array $definitions)
    {
        $this->definitions = $definitions;
        return $this;
    }

    /**
     * Has a definition
     *
     * @param  string $name
     * @return bool
     */
    public function hasDefinition(string $name)
    {
        return isset($this->definitions[$name]);
    }

    /**
     * Get a definition
     *
     * @param   string $name
     * @return  mixed
     */
    public function getDefinition(string $name)
    {
        if ($this->hasDefinition($name)) {
            return $this->definitions[$name];
        }
        return null;
    }

    /**
     * @return array
     */
    public function getDefinitions(): array
    {
        return $this->definitions;
    }

    /**
     * Add a singleton definition
     *
     * @param string $name
     * @param mixed $definition
     * @return $this
     */
    public function bindSingleton(string $name, $definition)
    {
        $this->definitions[$name] = new Singleton($name, $definition);
        return $this;
    }

    /**
     * Add a groups of singletons definitions
     *
     * @param array<string, mixed> $singletons
     * @return $this
     */
    public function bindSingletons(array $singletons)
    {
        foreach ($singletons as $name => $definition) {
            $this->bindSingleton($name, $definition);
        }
        return $this;
    }

    /**
     * Add a prototype definition
     *
     * @param string $name
     * @param mixed $definition
     * @return $this
     */
    public function bindPrototype(string $name, $definition)
    {
        $this->definitions[$name] = new Prototype($name, $definition);
        return $this;
    }

    /**
     * Add a groups of prototypes definitions
     *
     * @param array<string, mixed> $prototypes
     * @return $this
     */
    public function bindPrototypes(array $prototypes)
    {
        foreach ($prototypes as $name => $definition) {
            $this->bindPrototype($name, $definition);
        }
        return $this;
    }

    /**
     * Add a factory definition
     *
     * @param string $name
     * @param mixed $definition
     * @return $this
     */
    public function bindFactory(string $name, $definition)
    {
        $this->definitions[$name] = new Factory($name, $definition);
        return $this;
    }

    /**
     * Add a groups of factories definitions
     *
     * @param array<string, mixed> $factories
     * @return $this
     */
    public function bindFactories(array $factories)
    {
        foreach ($factories as $name => $definition) {
            $this->bindFactory($name, $definition);
        }
        return $this;
    }

    /**
     * Enable Auto Wiring
     *
     * @return $this
     */
    public function enableAutoWiring()
    {
        $this->auto_wiring = true;
        return $this;
    }

    /**
     * Disable Auto Wiring
     *
     * @return $this
     */
    public function disableAutoWiring()
    {
        $this->auto_wiring = false;
        return $this;
    }
}