Completed
Push — master ( 50f6f9...4cc575 )
by Mikael
01:40
created

RouteHandler::handle()   C

Complexity

Conditions 14
Paths 8

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 14

Importance

Changes 0
Metric Value
dl 0
loc 43
ccs 22
cts 22
cp 1
rs 6.2666
c 0
b 0
f 0
cc 14
nc 8
nop 5
crap 14

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
namespace Anax\Route;
4
5
use Anax\Commons\ContainerInjectableInterface;
6
use Anax\Route\Exception\ConfigurationException;
7
use Anax\Route\Exception\NotFoundException;
8
use Psr\Container\ContainerInterface;
9
10
/**
11
 * Call a routes handler and return the results.
12
 */
13
class RouteHandler
14
{
15
    /**
16
     * @var ContainerInterface $di the dependency/service container.
17
     */
18
    protected $di;
19
20
21
22
    /**
23
     * Handle the action for a route and return the results.
24
     *
25
     * @param string                       $method    the request method.
26
     * @param string                       $path      that was matched.
27
     * @param string|array                 $action    base for the callable.
28
     * @param array                        $arguments optional arguments.
29
     * @param ContainerInjectableInterface $di        container with services.
30
     *
31
     * @return mixed as the result from the route handler.
32
     */
33 142
    public function handle(
34
        string $method = null,
35
        string $path = null,
36
        $action,
37
        array $arguments = [],
38
        ContainerInterface $di = null
39
    ) {
40 142
        $this->di = $di;
41
42 142
        if (is_null($action)) {
43 1
            return;
44
        }
45
46 141
        if (is_callable($action)) {
47 117
            if (is_array($action)
48 117
                && is_string($action[0])
49 117
                && class_exists($action[0])
50
            ) {
51 2
                $action[] = $arguments;
52 2
                return $this->handleAsControllerAction($action);
53
            }
54 115
            return $this->handleAsCallable($action, $arguments);
55
        }
56
57 24
        if (is_string($action) && class_exists($action)) {
58 20
            $callable = $this->isControllerAction($method, $path, $action);
59 19
            if ($callable) {
60 18
                return $this->handleAsControllerAction($callable);
61
            }
62
        }
63
64 5
        if ($di
65 5
            && is_array($action)
66 5
            && isset($action[0])
67 5
            && isset($action[1])
68 5
            && is_string($action[0])
69
        ) {
70
            // Try to load service from app/di injected container
71 3
            return $this->handleUsingDi($action, $arguments, $di);
72
        }
73
        
74 2
        throw new ConfigurationException("Handler for route does not seem to be a callable action.");
75
    }
76
77
78
79
    /**
80
     * Get  an informative string representing the handler type.
81
     *
82
     * @param string|array                 $action    base for the callable.
83
     * @param ContainerInjectableInterface $di        container with services.
84
     *
85
     * @return string as the type of handler.
86
     */
87 2
    public function getHandlerType(
88
        $action,
89
        ContainerInterface $di = null
90
    ) {
91 2
        if (is_null($action)) {
92 1
            return "null";
93
        }
94
95 2
        if (is_callable($action)) {
96 1
            return "callable";
97
        }
98
99 2
        if (is_string($action) && class_exists($action)) {
100 1
            $callable = $this->isControllerAction(null, null, $action);
101 1
            if ($callable) {
102 1
                return "controller";
103
            }
104
        }
105
106 2
        if ($di
107 2
            && is_array($action)
108 2
            && isset($action[0])
109 2
            && isset($action[1])
110 2
            && is_string($action[0])
111 2
            && $di->has($action[0])
112 2
            && is_callable([$di->get($action[0]), $action[1]])
113
        ) {
114 1
            return "di";
115
        }
116
117 1
        return "not found";
118
    }
119
120
121
122
    /**
123
     * Check if items can be used to call a controller action, verify
124
     * that the controller exists, the action has a class-method to call.
125
     *
126
     * @param string $method the request method.
127
     * @param string $path   the matched path, base for the controller action
128
     *                       and the arguments.
129
     * @param string $class  the controller class
130
     *
131
     * @return array with callable details.
132
     */
133 21
    protected function isControllerAction(
134
        string $method = null,
135
        string $path = null,
136
        string $class
137
    ) {
138 21
        $method = ucfirst(strtolower($method));
139 21
        $args = explode("/", $path);
140 21
        $action = array_shift($args);
141 21
        $action = empty($action) ? "index" : $action;
142 21
        $action = str_replace("-", "", $action);
143 21
        $action1 = "{$action}Action{$method}";
144 21
        $action2 = "{$action}Action";
145 21
        $action3 = "catchAll{$method}";
146 21
        $action4 = "catchAll";
147
148 21 View Code Duplication
        foreach ([$action1, $action2] as $target) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
            try {
150 21
                $refl = new \ReflectionMethod($class, $target);
151 16
                if (!$refl->isPublic()) {
152 1
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
153
                }
154
155 15
                return [$class, $target, $args];
156 14
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
157
                ;
158
            }
159
        }
160
161 5 View Code Duplication
        foreach ([$action3, $action4] as $target) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
162
            try {
163 5
                $refl = new \ReflectionMethod($class, $target);
164 4
                if (!$refl->isPublic()) {
165
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
166
                }
167
168 4
                array_unshift($args, $action);
169 4
                return [$class, $target, $args];
170 2
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
171
                ;
172
            }
173
        }
174
175 1
        return false;
176
    }
177
178
179
180
    /**
181
     * Call the controller action with optional arguments and call
182
     * initialisation methods if available.
183
     *
184
     * @param string $callable with details on what controller action to call.
185
     *
186
     * @return mixed result from the handler.
187
     */
188 20
    protected function handleAsControllerAction(array $callable)
189
    {
190 20
        $class = $callable[0];
191 20
        $action = $callable[1];
192 20
        $args = $callable[2];
193 20
        $obj = new $class();
194
195 20
        $refl = new \ReflectionClass($class);
196 20
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
197 20
        $appInterface = "Anax\Commons\AppInjectableInterface";
198
199 20
        if ($this->di && $refl->implementsInterface($diInterface)) {
200 1
            $obj->setDI($this->di);
201 19
        } elseif ($this->di && $refl->implementsInterface($appInterface)) {
202 2
            if (!$this->di->has("app")) {
203 1
                throw new ConfigurationException(
204 1
                    "Controller '$class' implements AppInjectableInterface but \$app is not available in \$di."
205
                );
206
            }
207 1
            $obj->setApp($this->di->get("app"));
208
        }
209
210
        try {
211 19
            $refl = new \ReflectionMethod($class, "initialize");
212 12
            if ($refl->isPublic()) {
213 12
                $obj->initialize();
214
            }
215 7
        } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
216
            ;
217
        }
218
219 19
        $refl = new \ReflectionMethod($obj, $action);
220 19
        $paramIsVariadic = false;
221 19
        foreach ($refl->getParameters() as $param) {
222 9
            if ($param->isVariadic()) {
223 4
                $paramIsVariadic = true;
224 9
                break;
225
            }
226
        }
227
228 19
        if (!$paramIsVariadic
229 19
            && $refl->getNumberOfParameters() < count($args)
230
        ) {
231 1
            throw new NotFoundException(
232 1
                "Controller '$class' with action method '$action' valid but to many parameters. Got "
233 1
                . count($args)
234 1
                . ", expected "
235 1
                . $refl->getNumberOfParameters() . "."
236
            );
237
        }
238
239
        try {
240 18
            $res = $obj->$action(...$args);
241 2
        } catch (\ArgumentCountError $e) {
0 ignored issues
show
Bug introduced by
The class ArgumentCountError does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
242 1
            throw new NotFoundException($e->getMessage());
243 1
        } catch (\TypeError $e) {
244 1
            throw new NotFoundException($e->getMessage());
245
        }
246
247 16
        return $res;
248
    }
249
250
251
252
    /**
253
     * Handle as callable support callables where the method is not static.
254
     *
255
     * @param string|array                 $action    base for the callable
256
     * @param array                        $arguments optional arguments
257
     * @param ContainerInjectableInterface $di        container with services
0 ignored issues
show
Bug introduced by
There is no parameter named $di. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
258
     *
259
     * @return mixed as the result from the route handler.
260
     */
261 115
    protected function handleAsCallable(
262
        $action,
263
        array $arguments
264
    ) {
265 115
        if (is_array($action)
266 115
            && isset($action[0])
267 115
            && isset($action[1])
268 115
            && is_string($action[0])
269 115
            && is_string($action[1])
270 115
            && class_exists($action[0])
271
        ) {
272
            // ["SomeClass", "someMethod"] but not static
273
            $refl = new \ReflectionMethod($action[0], $action[1]);
274
            if ($refl->isPublic() && !$refl->isStatic()) {
275
                $obj = new $action[0]();
276
                return $obj->{$action[1]}(...$arguments);
277
            }
278
        }
279
280
        // Add $di to param list, if defined by the callback
281 115
        $refl = is_array($action)
282 1
            ? new \ReflectionMethod($action[0], $action[1])
283 115
            : new \ReflectionFunction($action);
284 115
        $params = $refl->getParameters();
285 115
        if (isset($params[0]) && $params[0]->getName() === "di") {
286 1
            array_unshift($arguments, $this->di);
287
        }
288
289 115
        return call_user_func($action, ...$arguments);
290
    }
291
292
293
294
    /**
295
     * Load callable as a service from the $di container.
296
     *
297
     * @param string|array                 $action    base for the callable
298
     * @param array                        $arguments optional arguments
299
     * @param ContainerInjectableInterface $di        container with services
300
     *
301
     * @return mixed as the result from the route handler.
302
     */
303 3
    protected function handleUsingDi(
304
        $action,
305
        array $arguments,
306
        ContainerInterface $di
307
    ) {
308 3
        if (!$di->has($action[0])) {
309 1
            throw new ConfigurationException("Routehandler '{$action[0]}' not loaded in di.");
310
        }
311
    
312 2
        $service = $di->get($action[0]);
313 2
        if (!is_callable([$service, $action[1]])) {
314 1
            throw new ConfigurationException(
315 1
                "Routehandler '{$action[0]}' does not have a callable method '{$action[1]}'."
316
            );
317
        }
318
    
319 1
        return call_user_func(
320 1
            [$service, $action[1]],
321 1
            ...$arguments
322
        );
323
    }
324
}
325