Passed
Push — master ( ade161...a5ce50 )
by Divine Niiquaye
02:26
created

CallableResolver::resolve()   C

Complexity

Conditions 12
Paths 13

Size

Total Lines 37
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 17
c 2
b 0
f 0
nc 13
nop 1
dl 0
loc 37
ccs 17
cts 17
cp 1
crap 12
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHP Invoker.
7
 *
8
 * PHP version 7.1 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace DivineNii\Invoker;
19
20
use Closure;
21
use DivineNii\Invoker\Exceptions\NotCallableException;
22
use Psr\Container\ContainerInterface;
23
use Psr\Container\NotFoundExceptionInterface;
24
use ReflectionClass;
25
use ReflectionMethod;
26
use Throwable;
27
28
/**
29
 * Resolves a callable from a container.
30
 *
31
 * @author Matthieu Napoli <[email protected]>
32
 * @author Divine Niiquaye Ibok <[email protected]>
33
 */
34
class CallableResolver
35
{
36
    public const CALLABLE_PATTERN = '!^([^\:]+)(:|@)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
37
38
    /**
39
     * @var null|ContainerInterface
40
     */
41
    private $container;
42
43 70
    public function __construct(?ContainerInterface $container = null)
44
    {
45 70
        $this->container = $container;
46 70
    }
47
48
    /**
49
     * Resolve the given callable into a real PHP callable.
50
     *
51
     * @param array<mixed,string>|callable|string $callable
52
     *
53
     * @throws NotCallableException
54
     *
55
     * @return callable real PHP callable
56
     */
57 50
    public function resolve($callable)
58
    {
59
        // Shortcut for a very common use case
60 50
        if ($callable instanceof Closure) {
61 11
            return $callable;
62
        }
63
64 39
        if (\is_string($callable) && 1 === \preg_match(self::CALLABLE_PATTERN, $callable, $matches)) {
65
            // check for callable as "class:method", and "class@method"
66 4
            $callable = [$matches[1], $matches[3]];
67
        }
68
69
        // The callable is a container entry name
70 39
        if (\is_string($callable) && null !== $this->container) {
71
            try {
72 5
                return $this->resolve($this->container->get($callable));
73 3
            } catch (NotFoundExceptionInterface $e) {
74 2
                if ($this->container->has($callable)) {
75 1
                    throw $e;
76
                }
77
78 1
                throw NotCallableException::fromInvalidCallable($callable, true);
79
            }
80
        }
81
82 37
        $callable = $this->resolveFromContainer($callable);
83
84
        // Callable object or string (i.e. implementing __invoke())
85 32
        if ((\is_string($callable) || \is_object($callable)) && \method_exists($callable, '__invoke')) {
86 6
            return $this->resolve([$callable, '__invoke']);
87
        }
88
89 32
        if (!\is_callable($callable)) {
90 10
            throw NotCallableException::fromInvalidCallable($callable, null !== $this->container);
91
        }
92
93 22
        return $callable;
94
    }
95
96
    /**
97
     * @param array<mixed,string>|callable|string $callable
98
     *
99
     * @throws NotCallableException
100
     *
101
     * @return array<mixed,string>|callable|string
102
     */
103 37
    private function resolveFromContainer($callable)
104
    {
105 37
        $isStaticCallToNonStaticMethod = false;
106
107
        // If it's already a callable there is nothing to do
108 37
        if (\is_callable($callable)) {
109 20
            $isStaticCallToNonStaticMethod = $this->isStaticCallToNonStaticMethod($callable);
110
111 20
            if (!$isStaticCallToNonStaticMethod) {
112 12
                return $callable;
113
            }
114
        }
115
116
        // The callable is an array whose first item is a container entry name
117
        // e.g. ['some-container-entry', 'methodToCall']
118 26
        if (\is_array($callable) && \is_string($callable[0])) {
119
            try {
120 18
                if (null !== $this->container) {
121
                    // Replace the container entry name by the actual object
122 10
                    $callable[0] = $this->container->get($callable[0]);
123
                }
124
125 13
                if (\is_string($callable[0]) && \class_exists($callable[0])) {
126 8
                    $callable[0] = (new ReflectionClass($callable[0]))->newInstance();
127
                }
128
129 13
                return $callable;
130 5
            } catch (Throwable $e) {
131 5
                if (null !== $this->container && $this->container->has($callable[0])) {
132 1
                    throw $e;
133
                }
134
135 4
                if ($e instanceof NotFoundExceptionInterface) {
136 2
                    if ($isStaticCallToNonStaticMethod) {
137 1
                        throw new NotCallableException(\sprintf(
138 1
                            'Cannot call %s::%s() because %2$s() is not a static method and "%1$s" is not a container entry',
139 1
                            $callable[0],
140 1
                            $callable[1]
141
                        ));
142
                    }
143
144 1
                    throw new NotCallableException(\sprintf(
145 1
                        'Cannot call %s on %s because it is not a class nor a valid container entry',
146 1
                        $callable[1],
147 1
                        $callable[0]
148
                    ));
149
                }
150
151 2
                throw NotCallableException::fromInvalidCallable($callable, null !== $this->container);
152
            }
153
        }
154
155
        // Unrecognized stuff, we let it fail later
156 10
        return $callable;
157
    }
158
159
    /**
160
     * Check if the callable represents a static call to a non-static method.
161
     *
162
     * @param mixed $callable
163
     *
164
     * @return bool
165
     */
166 20
    private function isStaticCallToNonStaticMethod($callable)
167
    {
168 20
        if (\is_array($callable) && \is_string($callable[0])) {
169 9
            list($class, $method) = $callable;
170 9
            $reflection           = new ReflectionMethod($class, $method);
171
172 9
            return !$reflection->isStatic();
173
        }
174
175 11
        return false;
176
    }
177
}
178