acelot/resolver

View on GitHub
src/Definition/FactoryDefinition.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php declare(strict_types = 1);

namespace Acelot\Resolver\Definition;

use Acelot\Resolver\Definition\Traits\ArgumentsTrait;
use Acelot\Resolver\Definition\Traits\ShareTrait;
use Acelot\Resolver\DefinitionInterface;
use Acelot\Resolver\Exception\DefinitionException;
use Acelot\Resolver\ResolverInterface;

class FactoryDefinition implements DefinitionInterface
{
    use ShareTrait;
    use ArgumentsTrait;

    protected const TYPE_UNKNOWN = -1;
    protected const TYPE_CLOSURE = 0;
    protected const TYPE_OBJECT = 1;
    protected const TYPE_ARRAY = 2;
    protected const TYPE_ARRAY_OBJECT = 3;
    protected const TYPE_STRING = 4;
    protected const TYPE_STRING_SEPARATED = 5;

    /**
     * @var callable
     */
    protected $callable;

    /**
     * Creates the definition with given callable.
     *
     * @param callable $callable Callable
     *
     * @return FactoryDefinition
     */
    public static function define(callable $callable): FactoryDefinition
    {
        return new FactoryDefinition($callable);
    }

    /**
     * @param callable $callable Callable
     */
    public function __construct(callable $callable)
    {
        $this->callable = $callable;
        $this->isShared = true;
    }

    /**
     * @return callable
     */
    public function getCallable(): callable
    {
        return $this->callable;
    }

    /**
     * @param callable $callable
     *
     * @return FactoryDefinition
     */
    public function withCallable(callable $callable): FactoryDefinition
    {
        $clone = clone $this;
        $clone->callable = $callable;

        return $clone;
    }

    /**
     * Resolves and invoke the callable.
     *
     * @param ResolverInterface $resolver
     *
     * @return object
     * @throws DefinitionException
     */
    public function resolve(ResolverInterface $resolver)
    {
        $type = self::getCallableType($this->callable);

        switch ($type) {
            case self::TYPE_CLOSURE:
            case self::TYPE_STRING:
                /** @var \Closure|callable-string $callable */
                $callable = $this->callable;
                $ref = new \ReflectionFunction($callable);
                $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver));

                return call_user_func($this->callable, ...$args);

            case self::TYPE_OBJECT:
                /** @var object $callable */
                $callable = $this->callable;
                $ref = new \ReflectionMethod($callable, '__invoke');
                $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver));

                return call_user_func($this->callable, ...$args);

            case self::TYPE_ARRAY_OBJECT:
                /** @var array $callable */
                $callable = $this->callable;
                /** @var object $callableObject */
                $callableObject = $callable[0];
                /** @var string $callableMethod */
                $callableMethod = $callable[1];
                $ref = new \ReflectionMethod($callableObject, $callableMethod);
                $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver));

                return $ref->invoke($callableObject, ...$args);

            case self::TYPE_ARRAY:
                /** @var array $callable */
                $callable = $this->callable;
                /** @var class-string $fqcn */
                $fqcn = $callable[0];
                /** @var string $method */
                $method = $callable[1];
                if ($method === '__construct') {
                    throw new DefinitionException('Use ObjectDefinition instead of FactoryDefinition');
                }

                $ref = new \ReflectionMethod($fqcn, $method);
                $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver));

                if ($ref->isStatic()) {
                    return call_user_func($this->callable, ...$args);
                }

                return $ref->invoke($resolver->resolve($fqcn), ...$args);

            case self::TYPE_STRING_SEPARATED:
                /** @var string $callable */
                $callable = $this->callable;

                /** @var class-string $fqcn */
                list($fqcn, $method) = explode('::', $callable);
                if ($method === '__construct') {
                    throw new DefinitionException('Use ObjectDefinition instead of FactoryDefinition');
                }

                $ref = new \ReflectionMethod($fqcn, $method);
                $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver));

                return call_user_func($this->callable, ...$args);

            default:
                throw new DefinitionException('Unknown callable type');
        }
    }

    /**
     * Returns the type of callable.
     *
     * @param callable $callable
     *
     * @return int
     *
     * @psalm-suppress RedundantCondition
     */
    protected static function getCallableType(callable $callable): int
    {
        // Closure
        if ($callable instanceof \Closure) {
            return self::TYPE_CLOSURE;
        }

        // Object
        if (is_object($callable)) {
            return self::TYPE_OBJECT;
        }

        // Array
        if (is_array($callable)) {
            if (is_object($callable[0])) {
                return self::TYPE_ARRAY_OBJECT;
            }

            return self::TYPE_ARRAY;
        }

        // String
        if (is_string($callable)) {
            if (strpos($callable, '::') !== false) {
                return self::TYPE_STRING_SEPARATED;
            }

            return self::TYPE_STRING;
        }

        return self::TYPE_UNKNOWN;
    }
}