eric-chau/jarvis

View on GitHub
src/Skill/DependencyInjection/Container.php

Summary

Maintainability
A
55 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Jarvis\Skill\DependencyInjection;

use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

/**
 * Minimalist dependency injection container
 *
 * @author Eric Chau <eriic.chau@gmail.com>
 */
class Container implements \ArrayAccess, ContainerInterface
{
    protected $locked = [];
    private $aliasOf = [];
    private $factories;
    private $hasAliases = [];
    private $raw = [];
    private $values = [];

    public function __construct()
    {
        $this->factories = new \SplObjectStorage();
    }

    /**
     * Checks if a parameter or an object is defined.
     *
     * @param  string $id the parameter/object identifier to check
     * @return boolean
     */
    public function offsetExists($id): bool
    {
        return in_array($id, $this->keys());
    }

    /**
     * Gets a parameter or an object.
     *
     * @param  string $id the parameter/object identifier
     * @return mixed The requested value
     * @throws \InvalidArgumentException if provided identifier is not defined
     */
    public function offsetGet($id)
    {
        $id = $this->resolveIdentifier($id);
        if (
            isset($this->raw[$id])
            || !is_object($this->values[$id])
            || !method_exists($this->values[$id], '__invoke')
        ) {
            return $this->values[$id];
        }

        if (isset($this->factories[$this->values[$id]])) {
            return $this->values[$id]($this);
        }

        $this->raw[$id] = $this->values[$id];

        return $this->values[$id] = $this->raw[$id]($this);
    }

    /**
     * Sets a parameter or an object.
     *
     * Note that you cannot override locked value, you have to call ::offsetUnset first.
     *
     * @param  string $id the identifier for parameter or object
     * @param  mixed  $v  the value of the parameter or an object
     * @throws \RuntimeException prevents override of locked value
     * @throws \InvalidArgumentException prevents value's identifier to be equal to an existing alias
     */
    public function offsetSet($id, $v): void
    {
        if (isset($this->locked[$id])) {
            throw new \RuntimeException(sprintf('Cannot override locked value "%s".', $id));
        }

        if (isset($this->aliasOf[$id])) {
            throw new \InvalidArgumentException("Value's identifier cannot be equal to existing alias.");
        }

        $this->values[$id] = $v;
    }

    /**
     * Unsets a parameter or an object. It can also unset an alias.
     *
     * Note that if you unset a value it will also unset all its aliases.
     *
     * @param  string $id the identifier of the object/parameter to unset
     */
    public function offsetUnset($id): void
    {
        if (isset($this->values[$id])) {
            if (is_object($this->values[$id])) {
                unset($this->factories[$this->values[$id]]);
            }

            if (isset($this->hasAliases[$id])) {
                foreach ($this->hasAliases[$id] as $alias) {
                    unset($this->aliasOf[$alias]);
                }
            }

            unset($this->values[$id], $this->raw[$id], $this->hasAliases[$id], $this->locked[$id]);
        } else {
            unset($this->aliasOf[$id]);
        }
    }

    /**
     * {@inheritdoc}
     * {@see ContainerInterface::get}
     *
     * This method has been added to be compatible with PSR-11.
     */
    public function get($id)
    {
        try {
            return $this->offsetGet($id);
        } catch (\InvalidArgumentException $exception) {
            throw new class($exception->getMessage())
                extends \InvalidArgumentException
                implements NotFoundExceptionInterface {}
            ;
        }
    }

    /**
     * {@inheritdoc}
     * {@see ContainerInterface::has}
     *
     * This method has been added to be compatible with PSR-11.
     */
    public function has($id)
    {
        return $this->offsetExists($id);
    }

    /**
     * Adds alias to service's identifier.
     *
     * @param  string $alias the alias to identifier
     * @param  string $id    the identifier to alias
     * @return self
     * @throws InvalidArgumentException if provided identifier is undefined or if alias is
     *                                  equals to identifier
     */
    public function alias(string $alias, string $id): void
    {
        if (!$this->offsetExists($id)) {
            throw new \InvalidArgumentException(sprintf('Cannot create alias for undefined value "%s".', $id));
        }

        if ($alias === $id || array_key_exists($alias, $this->values)) {
            throw new \InvalidArgumentException('Alias cannot be equals to value identifier.');
        }

        $this->aliasOf[$alias] = $id;
        $this->hasAliases[$id] = $this->hasAliases[$id] ?? [];
        $this->hasAliases[$id][] = $alias;
    }

    /**
     * Retrieves parameter and/or object by identifier.
     * this method also support wildcard character (*) in identifier.
     *
     * @param  string $id this identifier can contain one or many wildcard character (*)
     * @return array an array of values that mached with provided identifier pattern
     */
    public function find(string $id): array
    {
        $values = [];
        $pattern = str_replace(['.', '*'], ['\.', '[\w\-\.]*'], $id);
        foreach ($this->keys() as $id) {
            if (1 === preg_match(sprintf('/^%s$/', $pattern), $id)) {
                $values[] = $this->offsetGet($id);
            }
        }

        return $values;
    }

    /**
     * Returns all defined identifiers and aliases.
     *
     * @return array
     */
    public function keys(): array
    {
        return array_merge(array_keys($this->values), array_keys($this->aliasOf));
    }

    /**
     * Add provided service as factory.
     *
     * @param  string $id      the factory identifier
     * @param  mixed  $factory the factory object
     * @return self
     * @throws InvalidArgumentException if provided factory is not a Closure or not an invokable object
     */
    public function factory(string $id, $factory): void
    {
        if (!is_object($factory) || !method_exists($factory, '__invoke')) {
            throw new \InvalidArgumentException('Service factory must be a closure or an invokable object.');
        }

        $this->offsetSet($id, $factory);
        $this->factories->attach($factory);
    }

    /**
     * Locks an object or a parameter so you can not override it until unset() is called.
     *
     * @param  string|array $ids the identifier(s) to lock
     * @return self
     */
    public function lock($ids): void
    {
        foreach ((array) $ids as $id) {
            $this->locked[$this->resolveIdentifier($id)] = true;
        }
    }

    /**
     * Returns associated identifier if provided argument is an alias.
     *
     * @param  string $id the identifier to convert if needed
     * @return string
     * @throws \InvalidArgumentException if provided identifier is not defined
     */
    private function resolveIdentifier(string $id): string
    {
        if (!$this->offsetExists($id)) {
            throw new \InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id));
        }

        return $this->aliasOf[$id] ?? $id;
    }
}