bkdotcom/PHPDebugConsole

View on GitHub
src/Container/Container.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php

/**
 * This file is part of PHPDebugConsole
 *
 * @package   PHPDebugConsole
 * @author    Brad Kent <bkfake-github@yahoo.com>
 * @license   http://opensource.org/licenses/MIT MIT
 * @copyright 2014-2024 Brad Kent
 * @version   v3.0
 */

namespace bdk;

use bdk\Container\ServiceProviderInterface;
use OutOfBoundsException;

/**
 * Container
 *
 * Forked from pimple/pimple
 *    adds:
 *       get()
 *       has()
 *       needsInvoked()
 *       setCfg()
 *          allowOverride & onInvoke callback
 *       setValues()
 *
 * @author Fabien Potencier
 * @author Brad Kent <bkfake-github@yahoo.com>
 */
class Container implements \ArrayAccess
{
    /** @var array */
    private $cfg = array(
        'allowOverride' => false,  // whether can update alreay built service
        'onInvoke' => null, // callable
    );

    /**
     * Closures flagged as factories
     *
     * @var \SplObjectStorage
     */
    private $factories;

    /** @var array<string, bool> */
    private $invoked = array();  // keep track of invoked service closures

    /** @var array<string, bool> */
    private $keys = array();

    /**
     * wrap anonymous functions with the protect() method to store them as value
     *  vs treating as service
     *
     * @var \SplObjectStorage
     */
    private $protected;

    /** @var array<string, mixed> */
    private $raw = array();

    /** @var array<string, mixed> */
    private $values = array();

    /**
     * Instantiates the container.
     *
     * Objects and parameters can be passed as argument to the constructor.
     *
     * @param array $values The parameters or objects
     * @param array $cfg    Config options
     */
    public function __construct($values = array(), $cfg = array())
    {
        $this->factories = new \SplObjectStorage();
        $this->protected = new \SplObjectStorage();

        $this->setCfg($cfg);
        $this->setValues($values);
    }

    /**
     * Marks a callable as being a factory service.
     * A new instance will be returned each time it is accessed
     *
     *     $container['someFactory'] = $container->factory(static function () {
     *       return new FactoryThing();
     *     });
     *
     * @param callable $invokable A service definition to be used as a factory
     *
     * @return callable The passed callable
     * @throws \InvalidArgumentException Service definition has to be a closure or an invokable object
     */
    public function factory($invokable)
    {
        if (\is_object($invokable) === false || \method_exists($invokable, '__invoke') === false) {
            throw new \InvalidArgumentException('Closure or invokable object expected.');
        }
        $this->factories->attach($invokable);
        return $invokable;
    }

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $name Identifier of the entry to look for.
     *
     * @return mixed Entry.
     */
    public function get($name)
    {
        return $this->offsetGet($name);
    }

    /**
     * Do we have an entry for the given identifier.
     *
     * @param string $name Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has($name)
    {
        return $this->offsetExists($name);
    }

    /**
     * Returns all defined value names.
     *
     * @return array An array of value names
     */
    public function keys()
    {
        return \array_keys($this->values);
    }

    /**
     * Is value a service/factory that hasn't been invoked yet?
     *
     * @param string $name Identifier of entry to check
     *
     * @return bool
     *
     * @throws OutOfBoundsException If the identifier is not defined
     */
    public function needsInvoked($name)
    {
        $this->assertExists($name);
        $notNeedInvoked = isset($this->invoked[$name]) === true
            || \is_object($this->values[$name]) === false
            || \method_exists($this->values[$name], '__invoke') === false
            || isset($this->protected[$this->values[$name]]) === true;
        return $notNeedInvoked === false;
    }

    /**
     * ArrayAccess
     * Checks if a parameter or an object is set.
     *
     * @param string $name The unique identifier for the parameter or object
     *
     * @return bool
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($name)
    {
        return isset($this->keys[$name]);
    }

    /**
     * ArrayAccess
     * Gets a parameter or an object.
     *
     * @param string $name The unique identifier for the parameter or object
     *
     * @return mixed The value of the parameter or an object
     * @throws OutOfBoundsException If the identifier is not defined
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($name)
    {
        $this->assertExists($name);
        if ($this->needsInvoked($name) === false) {
            return $this->values[$name];
        }
        if (isset($this->factories[$this->values[$name]])) {
            // we're a factory
            $val = $this->values[$name]($this);
            if (\is_callable($this->cfg['onInvoke'])) {
                $this->cfg['onInvoke']($val, $name, $this);
            }
            return $val;
        }
        // we're a service
        $raw = $this->values[$name];
        $this->invoked[$name] = true;
        $this->raw[$name] = $raw;

        $val = $raw($this);
        if (\is_callable($this->cfg['onInvoke'])) {
            $this->cfg['onInvoke']($val, $name, $this);
        }
        $this->values[$name] = $val;

        return $val;
    }

    /**
     * ArrayAccess
     * Sets a parameter or an object.
     *
     * @param string $offset The unique identifier for the parameter or object
     * @param mixed  $value  The value of the parameter or a closure to define an object
     *
     * @throws \RuntimeException Prevent override of a already built service
     * @return void
     */
    #[\ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        if (isset($this->invoked[$offset]) && $this->cfg['allowOverride'] === false) {
            throw new \RuntimeException(
                \sprintf('Cannot update "%s" after it has been instantiated.', $offset)
            );
        }

        $this->keys[$offset] = true;
        $this->values[$offset] = $value;
        unset(
            $this->invoked[$offset],
            $this->raw[$offset]
        );
    }

    /**
     * ArrayAccess
     * Unsets a parameter or an object.
     *
     * @param string $name The unique identifier for the parameter or object
     *
     * @return void
     */
    #[\ReturnTypeWillChange]
    public function offsetUnset($name)
    {
        if ($this->offsetExists($name) === false) {
            return;
        }
        if (\is_object($this->values[$name])) {
            unset(
                $this->factories[$this->values[$name]],
                $this->protected[$this->values[$name]]
            );
        }
        unset(
            $this->invoked[$name],
            $this->keys[$name],
            $this->raw[$name],
            $this->values[$name]
        );
    }

    /**
     * Protects a callable from being interpreted as a service.
     *
     * This is useful when you want to store a callable as a value.
     *
     *     $container['some_func'] = $container->protect(static function () {
     *       return rand();
     *     });
     *
     * @param callable $invokable A callable to protect from being evaluated
     *
     * @return callable The passed callable
     * @throws \InvalidArgumentException Service definition has to be a closure or an invokable object
     */
    public function protect($invokable)
    {
        if (\is_object($invokable) === false || \method_exists($invokable, '__invoke') === false) {
            throw new \InvalidArgumentException('Closure or invokable object expected.');
        }
        $this->protected->attach($invokable);
        return $invokable;
    }

    /**
     * Gets a parameter or the closure defining an object.
     *
     * @param string $name The unique identifier for the parameter or object
     *
     * @return mixed The value of the parameter or the closure defining an object
     *
     * @throws OutOfBoundsException If the identifier is not defined
     */
    public function raw($name)
    {
        $this->assertExists($name);

        if (isset($this->raw[$name])) {
            return $this->raw[$name];
        }

        return $this->values[$name];
    }

    /**
     * Registers a service provider.
     *
     * @param ServiceProviderInterface $provider A ServiceProviderInterface instance
     *
     * @return $this
     */
    public function registerProvider(ServiceProviderInterface $provider)
    {
        $provider->register($this);
        return $this;
    }

    /**
     * Set one or more config values
     *
     *    setCfg('key', 'value')
     *    setCfg(array('k1'=>'v1', 'k2'=>'v2'))
     *
     * @param array|string $mixed key=>value array or key
     * @param mixed        $val   new value
     *
     * @return $this
     */
    public function setCfg($mixed, $val = null)
    {
        if (\is_string($mixed)) {
            $this->cfg[$mixed] = $val;
            return $this;
        }
        if (\is_array($mixed)) {
            $this->cfg = \array_merge($this->cfg, $mixed);
        }
        return $this;
    }

    /**
     * Set multiple values
     *
     * @param array $values values to set
     *
     * @return $this
     */
    public function setValues($values)
    {
        foreach ($values as $key => $value) {
            $this->offsetSet($key, $value);
        }
        return $this;
    }

    /**
     * Assert that the identifier exists
     *
     * @param string $name Identifier of entry to check
     *
     * @return void
     *
     * @throws OutOfBoundsException If the identifier is not defined
     */
    private function assertExists($name)
    {
        if ($this->offsetExists($name) === false) {
            throw new OutOfBoundsException(
                \sprintf('Unknown identifier: "%s"', $name)
            );
        }
    }
}