src/Container/Container.php
<?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
* @since v3.0
*/
namespace bdk;
use ArrayAccess;
use bdk\Container\ServiceProviderInterface;
use InvalidArgumentException;
use OutOfBoundsException;
use RuntimeException;
use SplObjectStorage;
/**
* 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 already built service
'onInvoke' => null, // callable
);
/**
* Closures used to modify / extend service definitions when invoked
*
* @var array<string,Closure>
*/
private $extenders;
/**
* Closures flagged as factories
*
* @var SplObjectStorage
*/
private $factories;
/**
* Keep track of invoked service closures
*
* @var array<string,bool>
*/
private $invoked = array();
/** @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;
/**
* Populated with the original raw service/factory closure when invoked
*
* @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);
}
/**
* Extends an object definition.
*
* Useful for
* - Extend an existing object definition without necessarily loading that object.
* - Ensure user-supplied factory is decorated with additional functionality.
*
* The callable should:
* - take the value as its first argument and the container as its second argument
* - return the modified value
*
* @param string $id The unique identifier for the object
* @param callable $callable A service definition to extend the original
*
* @return void
*/
public function extend($name, $callable)
{
$this->assertExists($name);
$this->assertInvokable($this->values[$name]);
$this->assertInvokable($callable);
$this->extenders[$name] = $callable;
}
/**
* 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)
{
$this->assertInvokable($invokable);
$this->factories->attach($invokable);
return $invokable;
}
/**
* Finds an entry 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);
return $this->onInvoke($name, $val);
}
// we're a service
$raw = $this->values[$name];
$this->invoked[$name] = true;
$this->raw[$name] = $raw;
$val = $raw($this);
$val = $this->onInvoke($name, $val);
$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)
{
$this->assertInvokable($invokable);
$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)
);
}
}
/**
* Assert that the identifier exists
*
* @param string $name Identifier of entry to check
*
* @return void
*
* @throws InvalidArgumentException If the identifier is not defined
*/
private function assertInvokable($val)
{
if (\is_object($val) === false || \method_exists($val, '__invoke') === false) {
throw new InvalidArgumentException(\sprintf(
'Closure or invokable object expected. %s provided',
$this->getDebugType($val)
));
}
}
/**
* Gets the type name of a variable in a way that is suitable for debugging
*
* @param mixed $value Value to inspect
*
* @return string
*/
protected static function getDebugType($value)
{
return \is_object($value)
? \get_class($value)
: \gettype($value);
}
/**
* Undocumented function
*
* @param string $name The service or factory name
* @param mixed $value The value returned by the definition
*
* @return mixed the value (possibly modified by extenders)
*/
private function onInvoke($name, $value)
{
if (isset($this->extenders[$name])) {
$callable = $this->extenders[$name];
$value = $callable($value, $this);
}
if (\is_callable($this->cfg['onInvoke'])) {
$this->cfg['onInvoke']($value, $name, $this);
}
return $value;
}
}