src/Resolver/Resolver.php
<?php
/**
*
*/
namespace Mvc5\Resolver;
use ArrayAccess;
use Closure;
use Mvc5\ArrayModel;
use Mvc5\Config\Configuration;
use Mvc5\Config\Model;
use Mvc5\Config\Scopable;
use Mvc5\Config\Scope;
use Mvc5\Container;
use Mvc5\Plugin\Gem\Args;
use Mvc5\Plugin\Gem\Call;
use Mvc5\Plugin\Gem\Calls;
use Mvc5\Plugin\Gem\Child;
use Mvc5\Plugin\Gem\Config;
use Mvc5\Plugin\Gem\Copy;
use Mvc5\Plugin\Gem\Expect;
use Mvc5\Plugin\Gem\Factory;
use Mvc5\Plugin\Gem\FileInclude;
use Mvc5\Plugin\Gem\Filter;
use Mvc5\Plugin\Gem\Gem;
use Mvc5\Plugin\Gem\Invokable;
use Mvc5\Plugin\Gem\Invoke;
use Mvc5\Plugin\Gem\Link;
use Mvc5\Plugin\Gem\Param;
use Mvc5\Plugin\Gem\Plug;
use Mvc5\Plugin\Gem\Plugin;
use Mvc5\Plugin\Gem\Provide;
use Mvc5\Plugin\Gem\Scoped;
use Mvc5\Plugin\Gem\Shared;
use Mvc5\Plugin\Gem\SignalArgs;
use Mvc5\Plugin\Gem\Value;
use Mvc5\Resolvable;
use Throwable;
use function array_merge;
use function array_shift;
use function explode;
use function is_array;
use function is_object;
use function is_string;
use function key;
use function substr;
use const Mvc5\{ ARGS, CALL_SEPARATOR, CALLS, CONTAINER, EVENTS, INDEX, MAX_RECURSION, NAME, PARAM,
PROPERTY, SERVICE_RESOLVER, SERVICE_SEPARATOR, SERVICES };
trait Resolver
{
/**
*
*/
use Base;
use Build;
use Generator;
use Scope;
use Service;
/**
* @var callable
*/
protected $provider;
/**
* @param array|ArrayAccess|null $config
* @param callable|null $provider
* @param bool|object|null $scope
* @param bool $strict
* @throws Throwable
*/
function __construct($config = null, callable $provider = null, $scope = null, bool $strict = false)
{
$this->config = $config instanceof Model ? $config : new ArrayModel((array) $config);
$this->container = ! isset($config[CONTAINER]) ? new Container :
($config[CONTAINER] instanceof Configuration ? $config[CONTAINER] : new Container((array) $config[CONTAINER]));
$this->events = ! isset($config[EVENTS]) ? new ArrayModel :
($config[EVENTS] instanceof ArrayAccess ? $config[EVENTS] : new ArrayModel((array) $config[EVENTS]));
$this->services = ! isset($config[SERVICES]) ? new ArrayModel :
($config[SERVICES] instanceof Model ? $config[SERVICES] : new ArrayModel((array) $config[SERVICES]));
$this->provider = $provider;
$this->strict = $strict;
$this->scope = $this->resolve($scope) ?? false;
}
/**
* @param array|mixed $args
* @return array|mixed
* @throws Throwable
*/
protected function args($args)
{
if (!$args) {
return $args;
}
if (!is_array($args)) {
return $this->resolve($args);
}
foreach($args as $index => $value) {
$value instanceof Resolvable && $args[$index] = $this->resolve($value);
}
return $args;
}
/**
* @param array $child
* @param array $parent
* @return array
*/
protected function arguments(array $child, array $parent) : array
{
return !$parent ? $child : (
!$child ? $parent : (is_string(key($child)) ? $child + $parent : [...$child, ...$parent])
);
}
/**
* @param Closure $callback
* @param object $object
* @param bool $scoped
* @return Closure
*/
protected function bind(Closure $callback, $object, bool $scoped) : Closure
{
return Closure::bind($callback, $object, $scoped ? $object : null);
}
/**
* @param Child $child
* @param array $args
* @return mixed
* @throws Throwable
*/
protected function child(Child $child, array $args = [])
{
return $this->provide($this->merge($this->parent($child->parent()), $child), $args);
}
/**
* @param mixed $value
* @param iterable $filters
* @param array $args
* @param string|null $param
* @return mixed
* @throws Throwable
*/
protected function filter($value, iterable $filters = [], array $args = [], string $param = null)
{
$result = $value;
foreach($filters as $filter) {
$value = $this->invoke(
$this->callable($filter), $param ? [$param => $result] + $args : [$result, ...$args]
);
if (false === $value) {
return $result;
}
if (null === $value) {
return null;
}
$result = $value;
}
return $result;
}
/**
* @param Filter $filter
* @param array $args
* @return mixed
* @throws Throwable
*/
protected function filterable(Filter $filter, array $args = [])
{
return $this->filter(
$this->resolve($filter->config()), $this->resolve($filter->plugin()), $args, $filter->param()
);
}
/**
* @param Gem $gem
* @param array $args
* @return callable|mixed
* @throws Throwable
*/
protected function gem(Gem $gem, array $args = [])
{
if ($gem instanceof Factory) {
return $this->invoke($this->child($gem, $args));
}
if ($gem instanceof Calls) {
return $this->hydrate($gem, $this->resolve($gem->name(), $args));
}
if ($gem instanceof Child) {
return $this->child($gem, $args);
}
if ($gem instanceof Plugin) {
return $this->provide($gem, $args);
}
if ($gem instanceof Shared) {
return $this->shared($gem->name(), $gem->config(), $args);
}
if ($gem instanceof Param) {
return $this->resolve($this->param($gem->name()), $args);
}
if ($gem instanceof Call) {
return $this->call($this->resolve($gem->config()), $this->vars($args, $gem->args()));
}
if ($gem instanceof Args) {
return $this->args($gem->config());
}
if ($gem instanceof Config) {
return $this->config;
}
if ($gem instanceof Link) {
return $this;
}
if ($gem instanceof Filter) {
return $this->filterable($gem, $this->vars($args, $gem->args()));
}
if ($gem instanceof Plug) {
return $this->configured($gem->name());
}
if ($gem instanceof Invoke) {
return fn(...$argv) => $this->resolve($this->call(
$this->resolve($gem->config()), $this->vars($this->variadic($argv), $gem->args())
));
}
if ($gem instanceof Invokable) {
return fn(...$argv) => $this->resolve($gem->config(), $this->vars($this->variadic($argv), $gem->args()));
}
if ($gem instanceof FileInclude) {
return (new class() {
function __invoke($file) {
return include $file;
}
})($this->resolve($gem->config()));
}
if ($gem instanceof Copy) {
return clone $this->resolve($gem->config(), $args);
}
if ($gem instanceof Value) {
return $gem->config();
}
if ($gem instanceof Scoped) {
return $this->scoped($gem->closure(), $gem->scoped());
}
if ($gem instanceof Provide) {
return ($this->provider() ?? new Unresolvable)($gem->config(), $this->vars($args, $gem->args()));
}
if ($gem instanceof Expect) {
try {
return $this->resolve($gem->plugin(), $args);
} catch(Throwable $exception) {
return $this->resolve($gem->exception(), $gem->args($exception, $args));
}
}
return Unresolvable::plugin($gem);
}
/**
* @param Plugin $plugin
* @param object $service
* @return object
* @throws Throwable
*/
protected function hydrate(Plugin $plugin, $service)
{
foreach($plugin->calls() as $method => $args) {
if (is_string($method)) {
if (INDEX == $method[0]) {
$service[substr($method, 1)] = $this->resolve($args);
continue;
}
if (PROPERTY == $method[0]) {
$service->{substr($method, 1)} = $this->resolve($args);
continue;
}
$service->$method($this->resolve($args));
continue;
}
if (is_array($args)) {
$method = array_shift($args);
$param = $plugin->param();
if (is_string($method) && PROPERTY == $method[0]) {
$param = substr($method, 1);
$method = array_shift($args);
}
$this->invoke(
is_string($method) ? [$service, $method] : $this->callable($method),
($param && (!$args || is_string(key($args))) ? [$param => $service] : []) + $this->args($args)
);
continue;
}
$this->resolve($args);
}
return $service;
}
/**
* @param Plugin $parent
* @param Plugin $child
* @param string|null $name
* @param array $config
* @return Plugin
* @throws Throwable
*/
protected function merge(Plugin $parent, Plugin $child, string $name = null, array $config = []) : Plugin
{
!$parent->name() &&
$config[NAME] = $name ?? $this->resolve($child->name());
$child->args() &&
$config[ARGS] = is_string(key($child->args())) ? $child->args() + $parent->args() : $child->args();
$child->calls() &&
$config[CALLS] = $child->merge() ? array_merge($parent->calls(), $child->calls()) : $child->calls();
$child->param() &&
$config[PARAM] = $child->param();
return $config ? $parent->with($config) : $parent;
}
/**
* @param array|string $name
* @return mixed
*/
function param($name)
{
if (is_string($name)) {
return param($this->config, $name);
}
$matched = [];
foreach($name as $key) {
$matched[$key] = $this->config[$key] ?? null;
}
return $matched;
}
/**
* @param string $parent
* @return Plugin
* @throws Throwable
*/
protected function parent(string $parent) : Plugin
{
return $this->configured($this->resolve($parent));
}
/**
* @param string|mixed $plugin
* @param array $args
* @param callable|null $callback
* @param string|null $previous
* @return mixed
* @throws Throwable
*/
function plugin($plugin, array $args = [], callable $callback = null, string $previous = null)
{
if (!$plugin) {
return $plugin;
}
if (is_string($plugin)) {
return $this->build(explode(SERVICE_SEPARATOR, $plugin), $args, $callback);
}
if (is_array($plugin)) {
return $this->pluginArray(array_shift($plugin), $args + $this->args($plugin), $callback, $previous);
}
if ($plugin instanceof Closure) {
return $this->invoke($this->scoped($plugin), $args);
}
return $this->resolve($plugin, $args);
}
/**
* @param string|mixed $plugin
* @param array $args
* @param callable|null $callback
* @param string|null $previous
* @return mixed
* @throws Throwable
*/
protected function pluginArray($plugin, array $args = [], callable $callback = null, string $previous = null)
{
return $previous && $previous === $plugin ?
$this->callback($plugin, true, $args, $callback) : $this->plugin($plugin, $args, $callback);
}
/**
* @param Plugin $plugin
* @param array $args
* @return callable|object|null
* @throws Throwable
*/
protected function provide(Plugin $plugin, array $args = [])
{
$name = $this->resolve($plugin->name());
$parent = $this->configured($name);
$args && is_string(key($args)) && $plugin->args() && $args += $this->args($plugin->args());
!$args && $args = $this->args($plugin->args());
if (!$parent) {
return $this->hydrate($plugin, $this->combine(explode(SERVICE_SEPARATOR, $name), $args));
}
if (!$parent instanceof Plugin) {
return $this->hydrate(
$plugin, $name === $parent ? $this->make($name, $args) : $this->provision($this->resolve($parent), $args)
);
}
if ($name === $parent->name()) {
return $this->hydrate($plugin, $this->make($name, $args));
}
return $this->provide($this->merge($parent, $plugin, $name), $args);
}
/**
* @return callable|null
*/
protected function provider() : ?callable
{
return $this->provider;
}
/**
* @param $plugin
* @param array $args
* @return mixed
* @throws \ReflectionException
* @throws Throwable
*/
protected function provision($plugin, array $args)
{
return $plugin instanceof Closure && (new \ReflectionFunction($plugin))->getClosureThis() ?
$this->invoke($plugin, $args) : $this->plugin($plugin, $args);
}
/**
* @param Resolvable|mixed $plugin
* @param array $args
* @param callable|null $callback
* @param int $c
* @return mixed
* @throws Throwable
*/
protected function resolvable($plugin, array $args = [], callable $callback = null, int $c = 0)
{
return !$plugin instanceof Resolvable ? $plugin : (
$c > MAX_RECURSION ? Unresolvable::plugin($plugin) :
$this->resolvable($this->solve($plugin, $args, $callback), $args, $callback, ++$c)
);
}
/**
* @param Resolvable|mixed $plugin
* @param array $args
* @return mixed
* @throws Throwable
*/
protected function resolve($plugin, array $args = [])
{
return $this->resolvable($plugin, $args);
}
/**
* @param string|mixed $plugin
* @param array $args
* @return mixed
* @throws Throwable
*/
protected function resolver($plugin, array $args = [])
{
return $this->call($this->provider() ?? SERVICE_RESOLVER, [$plugin, $args]);
}
/**
* @param Closure $callback
* @param bool $scoped
* @return Closure
*/
protected function scoped(Closure $callback, bool $scoped = false) : Closure
{
return $this->scope ? $this->bind($callback, $this->scope === true ? $this : $this->scope, $scoped) : $callback;
}
/**
* @param Gem|mixed $plugin
* @param array $args
* @param callable|null $callback
* @return callable|mixed
* @throws Throwable
*/
protected function solve($plugin, array $args = [], callable $callback = null)
{
return $plugin instanceof Gem ? $this->gem($plugin, $args) : (
$callback ? $callback($plugin, $args) : $this->resolver($plugin, $args)
);
}
/**
* @param array $args
* @return array
*/
protected function variadic(array $args) : array
{
return $args && $args[0] instanceof SignalArgs ? $args[0]->args() : $args;
}
/**
* @param array $child
* @param array $parent
* @return array
* @throws Throwable
*/
protected function vars(array $child = [], array $parent = []) : array
{
return $this->arguments($child, $this->args($parent));
}
/**
* @param mixed $plugin
* @param array $args
* @return mixed
* @throws Throwable
*/
function __call($plugin, array $args = [])
{
return $this->call($plugin, $args);
}
/**
*
*/
function __clone()
{
$this->config = clone $this->config;
$this->container = clone $this->container;
$this->events = clone $this->events;
$this->services = clone $this->services;
is_object($this->scope) &&
$this->scope = $this->scope instanceof Scopable ? $this->scope->withScope($this) : clone $this->scope;
}
/**
* @param mixed $plugin
* @param array $args
* @return mixed
* @throws Throwable
*/
function __invoke($plugin, array $args = [])
{
return $this->plugin($plugin, $args, $this->provider() ?? function(){});
}
/**
* @return array
*/
function __serialize() : array
{
return [$this->config, $this->events, $this->services, new Container, $this->provider, $this->scope, $this->strict];
}
/**
* @param array $data
*/
function __unserialize(array $data) : void
{
list($this->config, $this->events, $this->services, $this->container, $this->provider, $this->scope, $this->strict) = $data;
}
}
/**
* @param array|ArrayAccess $config
* @param string $name
* @return mixed
*/
function param($config, string $name)
{
$name = explode(CALL_SEPARATOR, $name);
$value = $config[array_shift($name)];
foreach($name as $n) {
$value = $value[$n];
}
return $value;
}