<?php
namespace Uid\Utils\Container;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionParameter;
use function array_key_exists;
use function array_map;
use function class_exists;
use function class_implements;
use function class_parents;
use function count;
use function get_class;
use function in_array;
use function is_object;
use function is_string;
use function method_exists;
use function sprintf;
/** @inheritdoc*/
class Container implements ContainerInterface
{
private array $definitions;
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];
}
public function value(string $name, $value): self
{
$this->definitions[$name] = ['value' => $value];
return $this;
}
public function alias(string $name, string $alias): self
{
$this->definitions[$name] = ['alias' => $alias];
return $this;
}
public function create(string $class, array $constants = [], bool $singleton = false): self
{
return $this->set($class, $class, $constants, $singleton);
}
public function factory(string $name, callable $callback, array $constants = [], bool $singleton = false): self
{
$this->definitions[$name] = ['callback' => $this->callback($callback, $constants), 'singleton' => $singleton];
return $this;
}
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;
}
/** @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));
}
if (array_key_exists('value', $definition)) {
return $definition['value'];
}
$result = ($definition['callback'])($this);
if (isset($definition['singleton'])) {
$this->definitions[$name]['value'] = $result;
}
return $result;
}
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));
}
private function definition(string $name): ?array
{
if (array_key_exists($name, $this->definitions)) {
return $this->definitions[$name];
} elseif (class_exists($name)) {
$this->definitions[$name] = ['callback' => $this->constructor($name), 'singleton' => false];
return $this->definitions[$name];
}
return null;
}
private function resolve(string $id): string
{
while (array_key_exists($id, $this->definitions)) {
if (array_key_exists('alias', $this->definitions[$id])) {
$id = $this->definitions[$id]['alias'];
} else {
return $id;
}
}
return $id;
}
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));
}
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 && self::isImplements($parameters[0]->getType(), self::class)) {
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));
}
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()) || class_exists($typename))) {
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));
}
private static function isImplements(string $type, string $class): bool
{
return $type == $class
|| in_array($type, class_parents($class))
|| in_array($type, class_implements($class));
}
}