<?php
namespace Uid\Utils\Container;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
use Throwable;
use function array_key_exists;
use function array_map;
use function class_exists;
use function count;
use function get_class;
use function is_a;
use function is_object;
use function is_string;
use function method_exists;
use function sprintf;
/** @inheritdoc*/
class Container implements ContainerInterface
{
private array $definitions;
/**
* Container constructor.
*
* @param array $definitions Predefined definitions for the container (default: []).
*/
public function __construct(array $definitions = [])
{
$this->definitions = $definitions;
$this->definitions[ContainerInterface::class] = ['alias' => Container::class];
$this->definitions[PsrContainerInterface::class] = ['alias' => Container::class];
$this->definitions[self::class] = ['value' => $this];
}
/**
* Define a value to be stored in the container.
*
* @param string $name The name of the value.
* @param mixed $value The value to store.
* @return Container The container instance.
*/
public function value(string $name, mixed $value): self
{
$this->definitions[$name] = ['value' => $value];
return $this;
}
/**
* Define an alias for a name in the container.
*
* @param string $name The name of the alias.
* @param string $alias The name of the entry to alias.
* @return Container The container instance.
*/
public function alias(string $name, string $alias): self
{
$this->definitions[$name] = ['alias' => $alias];
return $this;
}
/**
* Defines a class and store it in a container.
*
* @param string $class The name of the class.
* @param array $constants Parameters as an associative array to pass to the constructor (default: []).
* @param bool $singleton Whether the instance should be treated as a singleton (default: false).
* @return Container The container instance.
*/
public function create(string $class, array $constants = [], bool $singleton = false): self
{
return $this->set($class, $class, $constants, $singleton);
}
/**
* Define a factory callback to create instances in the container.
*
* @param string $name The name of the entry.
* @param callable $callback The factory callback.
* @param array $constants Parameters as an associative array to pass to the callback (default: []).
* @param bool $singleton Whether the instance should be treated as a singleton (default: false).
* @return Container The container instance.
*/
public function factory(string $name, callable $callback, array $constants = [], bool $singleton = false): self
{
return $this->set($name, $callback, $constants, $singleton);;
}
/**
* Define an entry in the container.
*
* @param string $name The name of the entry.
* @param string|callable $value The value or factory callback for the entry.
* @param array $constants Parameters as an associative array to pass to the constructor or callback (default: []).
* @param bool $singleton Whether the instance should be treated as a singleton (default: false).
* @return Container The container instance.
*/
public function set(string $name, string|callable $value, array $constants = [], bool $singleton = false): self
{
$this->definitions[$name] = [
'callback' => is_string($value)
? $this->constructor($value, $constants)
: $this->callback($value, $constants),
'singleton' => $singleton
];
return $this;
}
/** @inheritdoc*/
public function has(string $id): bool
{
//return $this->definition($this->resolve($id)) !== null;
return isset($this->definitions[$name = $this->resolve($id)]) || class_exists($name);
}
/** @inheritdoc*/
public function get(string $id): mixed
{
$name = $this->resolve($id);
$definition = $this->definition($name);
if ($definition === null) {
throw new NotFoundException(sprintf("Cannot find definition for '%s'", $name));
} elseif (array_key_exists('value', $definition)) {
return $definition['value'];
}
try {
$result = ($definition['callback'])($this);
} catch (Throwable $ex) {
throw new ContainerException(sprintf("Failed to create entity '%s'", $name), 0, $ex);
}
if (isset($definition['singleton'])) {
$this->definitions[$name]['value'] = $result;
}
return $result;
}
/** @inheritdoc*/
public function invoke(string|object $entry, string $method = '__invoke', array $constants = []): mixed
{
$object = is_string($entry) ? $this->get($entry) : $entry;
if (!is_object($object)) {
throw new ContainerException(sprintf("Unable to invoke method '%s' not exists for non-object '%s'", $method, $entry));
}
if (!method_exists($object, $method)) {
throw new NotFoundException(sprintf("Method '%s' does not exist for instance of '%s'", $method, get_class($object)));
}
try {
$reflection = new ReflectionMethod($object, $method);
} catch (ReflectionException $ex) {
throw new ContainerException(sprintf("Unable to invoke method '%s' for instance of '%s'", $method, get_class($object)), 0, $ex);
}
$arguments = array_map(
fn($parameter) => $this->argument($parameter, $constants),
$reflection->getParameters()
);
return $object->$method(... array_map(
fn($arg) => $arg[0] ? $this->get($arg[1]): $arg[1],
$arguments
));
}
/**
* Get the definition for the given entry name.
*
* @param string $name The name of the entry.
* @return array|null The definition array if found, null otherwise.
*/
private function definition(string $name): ?array
{
if (isset($this->definitions[$name])) {
return $this->definitions[$name];
} elseif (class_exists($name)) {
$this->definitions[$name] = [
'callback' => $this->constructor($name),
'singleton' => false
];
return $this->definitions[$name];
}
return null;
}
/**
* Resolve an entry name by following any aliases.
*
* @param string $id The name of the entry.
* @return string The resolved name.
*/
private function resolve(string $id): string
{
while (isset($this->definitions[$id])) {
if (isset($this->definitions[$id]['alias'])) {
$id = $this->definitions[$id]['alias'];
} else {
return $id;
}
}
return $id;
}
/**
* Create a constructor callback for the given class.
*
* @param string $class The name of the class.
* @param array $constants Parameters as an associative array to pass to the constructor (default: []).
* @return callable The constructor callback.
* @throws ContainerException If the class cannot be instantiated.
*/
private function constructor(string $class, array $constants = []): callable
{
try {
$reflection = new ReflectionClass($class);
} catch (ReflectionException $e) {
throw new ContainerException(sprintf("Unable to create object '%s'", $class), 0, $e);
}
$arguments = [];
if (($constructor = $reflection->getConstructor())) {
$arguments = array_map(
fn($parameter) => $this->argument($parameter, $constants),
$constructor->getParameters()
);
}
return fn(Container $container) => new $class(... array_map(
fn($arg) => $arg[0] ? $container->get($arg[1]) : $arg[1],
$arguments
));
}
/**
* Create a callback for the given callable.
*
* @param callable $callback The callable.
* @param array $constants Parameters as an associative array to pass to the callable (default: []).
* @return callable The created callback.
*/
private function callback(callable $callback, array $constants = []): callable
{
try {
$reflection = new ReflectionFunction($callback);
} catch (ReflectionException $ex) {
throw new ContainerException("Unable to prepare factory", 0, $ex);
}
$parameters = $reflection->getParameters();
if (empty($parameters)) {
return $callback;
}
if (count($parameters) === 1 && is_a($this, $parameters[0]->getType())) {
return $callback;
}
$arguments = array_map(
fn($parameter) => $this->argument($parameter, $constants),
$parameters
);
return fn(Container $container) => $callback(... array_map(
fn($arg) => $arg[0] ? $container->get($arg[1]): $arg[1],
$arguments
));
}
/**
* Resolve the argument for a parameter.
*
* @param ReflectionParameter $parameter The parameter reflection.
* @param array $constants Parameters as an associative array to pass to the callable or constructor.
* @return array The resolved argument, consisting of a flag indicating if the argument is a dependency and the value.
* @throws ContainerException If the argument cannot be resolved.
*/
private function argument(ReflectionParameter $parameter, array $constants): ?array
{
$name = $parameter->getName();
if (array_key_exists($name, $constants)) {
return [false, $constants[$name]];
} elseif (($type = $parameter->getType()) && !$type->isBuiltin() && $this->has($typename = $type->getName())) {
return [true, $typename];
} elseif ($parameter->isDefaultValueAvailable()) {
return [false, $parameter->getDefaultValue()];
}
throw new ContainerException(sprintf("Failed to process a parameter: '%s' of type '%s'", $name, $type));
}
}