venta/framework

View on GitHub
src/Container/src/Invokable.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php declare(strict_types = 1);

namespace Venta\Container;

use Closure;
use InvalidArgumentException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;

/**
 * Class Invokable
 *
 * @package Venta\Container
 */
class Invokable
{

    /**
     * @var callable
     */
    private $callable;

    /**
     * @var ReflectionFunctionAbstract
     */
    private $reflection;

    /**
     * ReflectedCallable constructor.
     *
     * @param callable|ReflectionFunctionAbstract $callable
     */
    public function __construct($callable)
    {
        if ($callable instanceof ReflectionFunctionAbstract) {
            $this->callable = $this->reflectionCallable($callable);
            $this->reflection = $callable;
        } else {
            $this->callable = $this->normalizeCallable($callable);
        }
    }

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

    /**
     * @return bool
     */
    public function isFunction(): bool
    {
        return !is_array($this->callable);
    }

    /**
     * @return ReflectionFunctionAbstract|ReflectionMethod|ReflectionFunction
     */
    public function reflection(): ReflectionFunctionAbstract
    {
        if (empty($this->reflection)) {
            $this->reflection = $this->isFunction()
                ? new ReflectionFunction($this->callable)
                : new ReflectionMethod($this->callable[0], $this->callable[1]);
        }

        return $this->reflection;
    }

    /**
     * @param $callable
     * @return callable
     * @throws InvalidArgumentException
     */
    private function normalizeCallable($callable)
    {
        if (is_object($callable)) {
            if ($callable instanceof Closure) {
                return $callable;
            } else {
                if (!method_exists($callable, '__invoke')) {
                    throw new InvalidArgumentException('Invalid callable provided.');
                }

                // Callable object is an instance with magic __invoke() method.
                return [$callable, '__invoke'];
            }
        }

        if (is_string($callable)) {
            // Existing function is always callable.
            if (function_exists($callable)) {
                return $callable;
            }
            if (method_exists($callable, '__invoke')) {
                // We allow to call class by name if `__invoke()` method is implemented.
                return [$callable, '__invoke'];
            }
            if (strpos($callable, '::') !== false) {
                // Replace "ClassName::methodName" string with ["ClassName", "methodName"] array.
                $callable = explode('::', $callable);
            }
        }

        // Is correct callable array.
        if (is_array($callable) && isset($callable[0], $callable[1]) && method_exists($callable[0], $callable[1])) {
            return $callable;
        }

        throw new InvalidArgumentException('Invalid callable provided.');
    }

    /**
     * @param ReflectionFunctionAbstract $reflection
     * @return array|Closure|string
     * @throws InvalidArgumentException
     */
    private function reflectionCallable(ReflectionFunctionAbstract $reflection)
    {
        if ($reflection instanceof ReflectionMethod) {
            return [$reflection->class, $reflection->name];
        }
        if ($reflection instanceof ReflectionFunction) {
            return $reflection->isClosure() ? $reflection->getClosure() : $reflection->name;
        }

        throw new InvalidArgumentException('Invalid callable provided.');
    }
}