Passed
Push — master ( b202e3...14e011 )
by Divine Niiquaye
02:35 queued 13s
created

CallableResolver::resolve()   B

Complexity

Conditions 10
Paths 16

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 10
nc 16
nop 2
dl 0
loc 24
ccs 11
cts 11
cp 1
crap 10
rs 7.6666
c 1
b 0
f 0

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 Flight Routing.
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 Flight\Routing;
19
20
use Closure;
21
use Flight\Routing\Exceptions\InvalidControllerException;
22
use Flight\Routing\Interfaces\CallableResolverInterface;
23
use Psr\Container\ContainerInterface;
24
use Psr\Http\Server\RequestHandlerInterface;
25
use ReflectionClass;
26
use RuntimeException;
27
use TypeError;
28
29
/**
30
 * This class resolves a string of the format 'class:method',
31
 * and 'class@method' into a closure that can be dispatched.
32
 *
33
 * @author Divine Niiquaye Ibok <[email protected]>
34
 *
35
 * @final
36
 */
37
class CallableResolver implements CallableResolverInterface
38
{
39
    public const CALLABLE_PATTERN = '!^([^\:]+)(:|@)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
40
41
    /**
42
     * @var null|ContainerInterface
43
     */
44
    protected $container;
45
46
    /**
47
     * @var null|object
48
     */
49
    protected $instance;
50
51
    /**
52
     * @param null|ContainerInterface $container
53
     */
54 18
    public function __construct(ContainerInterface $container = null)
55
    {
56 18
        $this->container = $container;
57 18
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62 1
    public function addInstanceToClosure($instance): CallableResolverInterface
63
    {
64 1
        $this->instance = $instance;
65
66 1
        return $this;
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 17
    public function resolve($toResolve, ?string $namespace = null): callable
73
    {
74 17
        if (null !== $namespace && (\is_string($toResolve) || !$toResolve instanceof Closure)) {
75 1
            $toResolve = $this->appendNamespace($toResolve, (string) $namespace);
76
        }
77
78 17
        if (\is_string($toResolve) && 1 === \preg_match(self::CALLABLE_PATTERN, $toResolve, $matches)) {
79
            // check for slim callable as "class:method", and "class@method"
80 2
            $toResolve = $this->resolveCallable($matches[1], $matches[3]);
81
        }
82
83 17
        if (\is_array($toResolve) && \count($toResolve) === 2 && \is_string($toResolve[0])) {
84 5
            $toResolve = $this->resolveCallable($toResolve[0], $toResolve[1]);
85
        }
86
87
        // Checks if indeed what wwe want to return is a callable.
88 16
        $toResolve = $this->assertCallable($toResolve);
89
90
        // Bind new Instance or $this->container to \Closure
91 15
        if ($toResolve instanceof Closure) {
92 2
            $toResolve = $toResolve->bindTo($this->instance ?? $this->container);
93
        }
94
95 15
        return $toResolve;
96
    }
97
98
    /**
99
     * Check if string is something in the DIC
100
     * that's callable or is a class name which has an __invoke() method.
101
     *
102
     * @param object|string $class
103
     * @param string        $method
104
     *
105
     * @throws InvalidControllerException if the callable does not exist
106
     * @throws TypeError                  if does not return a callable
107
     *
108
     * @return callable
109
     */
110 10
    protected function resolveCallable($class, $method = '__invoke'): callable
111
    {
112 10
        if (\is_string($class) && null !== $this->container && $this->container->has($class)) {
113 1
            $class = $this->container->get($class);
114
        }
115
116
        // We do not need class as a string here
117 10
        if (\is_string($class) && \class_exists($class)) {
118 7
            $class = (new ReflectionClass($class))->newInstance();
119
        }
120
121
        // For a class that implements RequestHandlerInterface, we will call handle()
122
        // if no method has been specified explicitly
123 10
        if ($class instanceof RequestHandlerInterface) {
124 5
            $method = 'handle';
125
        }
126
127 10
        if (\is_callable($callable = [$class, $method])) {
128 9
            return $callable;
129
        }
130
131 1
        throw new InvalidControllerException('Controller could not be resolved as callable');
132
    }
133
134
    /**
135
     * @param null|callable|object|string|string[] $controller
136
     * @param string                               $namespace
137
     *
138
     * @return null|callable|object|string|string[]
139
     */
140 1
    protected function appendNamespace($controller, string $namespace)
141
    {
142 1
        if (\is_string($controller) && !\class_exists($controller) && false === \stripos($controller, $namespace)) {
143 1
            $controller = \is_callable($controller) ? $controller : $namespace . $controller;
144
        }
145
146 1
        if (\is_array($controller) && (!\is_object($controller[0]) && !\class_exists($controller[0]))) {
147 1
            $controller[0] = $namespace . $controller[0];
148
        }
149
150 1
        return $controller;
151
    }
152
153
    /**
154
     * @param callable $callable
155
     *
156
     * @throws RuntimeException if the callable is not resolvable
157
     *
158
     * @return callable
159
     */
160 16
    protected function assertCallable($callable): callable
161
    {
162 16
        if (\is_string($callable) && null !== $this->container && $this->container->has($callable)) {
163 1
            $callable = $this->container->get($callable);
164
        }
165
166
        // Maybe could be a class object or RequestHandlerInterface instance
167 16
        if (!$callable instanceof Closure && \is_object($callable)) {
168 3
            $callable = $this->resolveCallable($callable);
169
        }
170
171
        // Maybe could be a class string or RequestHandlerInterface instance as string
172 16
        if (\is_string($callable) && \class_exists($callable)) {
173 2
            $callable = $this->resolveCallable($callable);
174
        }
175
176 16
        if (!\is_callable($callable)) {
177 1
            throw new InvalidControllerException(
178 1
                \sprintf('%s is not resolvable', \json_encode($callable) ?? $callable)
0 ignored issues
show
Bug introduced by
It seems like json_encode($callable) ?? $callable can also be of type callable; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

178
                \sprintf('%s is not resolvable', /** @scrutinizer ignore-type */ \json_encode($callable) ?? $callable)
Loading history...
179
            );
180
        }
181
182 15
        return $callable;
183
    }
184
}
185