src/Container/ParamResolver.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * This file is part of Railt package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
declare(strict_types=1);

namespace Railt\Container;

use Railt\Container\Exception\ParameterResolutionException;
use Railt\Dumper\TypeDumper;

/**
 * Class ParamResolver
 */
class ParamResolver
{
    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * ParamResolver constructor.
     *
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    /**
     * @param callable $action
     * @param array $additional
     * @return array
     * @throws \ReflectionException
     */
    public function fromCallable(callable $action, array $additional = []): array
    {
        return $this->fromClosure(\Closure::fromCallable($action), $additional);
    }

    /**
     * @param \Closure $action
     * @param array $additional
     * @return array
     * @throws \ReflectionException
     */
    public function fromClosure(\Closure $action, array $additional = []): array
    {
        return $this->resolve(new \ReflectionFunction($action), $additional);
    }

    /**
     * @param \ReflectionFunctionAbstract $reflection
     * @param array $additional
     * @return array
     */
    public function resolve(\ReflectionFunctionAbstract $reflection, array $additional = []): array
    {
        $result = [];

        foreach ($reflection->getParameters() as $parameter) {
            $result[] = $this->resolveParameter($parameter, $additional);
        }

        return $result;
    }

    /**
     * @param \ReflectionParameter $parameter
     * @param array $additional
     * @return mixed
     */
    private function resolveParameter(\ReflectionParameter $parameter, array $additional)
    {
        return $this->resolveByTypeHint($parameter, $additional);
    }

    /**
     * @param \ReflectionParameter $parameter
     * @param array $additional
     * @return mixed|object
     */
    private function resolveByTypeHint(\ReflectionParameter $parameter, array $additional)
    {
        if ($parameter->hasType()) {
            /** @noinspection NullPointerExceptionInspection */
            $name = $parameter->getType()->getName();

            return $this->resolveParameterByName($name, $additional, function () use ($parameter, $additional) {
                return $this->resolveByName($parameter, $additional);
            });
        }

        return $this->resolveByName($parameter, $additional);
    }

    /**
     * @param string $name
     * @param array $additional
     * @param \Closure $otherwise
     * @return mixed
     */
    private function resolveParameterByName(string $name, array $additional, \Closure $otherwise)
    {
        /** @noinspection NotOptimalIfConditionsInspection */
        if (isset($additional[$name]) || \array_key_exists($name, $additional)) {
            return $additional[$name];
        }

        if ($this->container->has($name)) {
            return $this->container->get($name);
        }

        return $otherwise($name, $additional);
    }

    /**
     * @param \ReflectionParameter $parameter
     * @param array $additional
     * @return mixed|object
     */
    private function resolveByName(\ReflectionParameter $parameter, array $additional)
    {
        $name = '$' . $parameter->getName();

        return $this->resolveParameterByName($name, $additional, function () use ($parameter) {
            return $this->resolveDefault($parameter);
        });
    }

    /**
     * @param \ReflectionParameter $parameter
     * @return mixed|null
     * @throws ParameterResolutionException
     * @throws \ReflectionException
     */
    private function resolveDefault(\ReflectionParameter $parameter)
    {
        if ($parameter->isOptional()) {
            return $parameter->getDefaultValue();
        }

        if ($parameter->allowsNull() && $parameter->hasType()) {
            return null;
        }

        throw $this->parameterError($parameter);
    }

    /**
     * @param \ReflectionParameter $param
     * @return ParameterResolutionException
     */
    private function parameterError(\ReflectionParameter $param): ParameterResolutionException
    {
        $position = $param->getPosition();
        $function = $param->getDeclaringFunction()->getName();

        $error = 'Can not resolve parameter (#%d %s) which is required in %s(...)';
        $error = \sprintf($error, $position, TypeDumper::render($param), $function);

        return ParameterResolutionException::fromReflectionFunction($error, $param->getDeclaringFunction());
    }

    /**
     * @param string $class
     * @param array $additional
     * @return array
     * @throws ParameterResolutionException
     * @throws \ReflectionException
     */
    public function fromConstructor(string $class, array $additional = []): array
    {
        if (\method_exists($class, '__construct')) {
            return $this->resolve((new \ReflectionClass($class))->getMethod('__construct'), $additional);
        }

        $parent = \get_parent_class($class);

        if ($parent === false) {
            return [];
        }

        return $this->fromConstructor($parent, $additional);
    }
}