RouteHandler   F
last analyzed

Complexity

Total Complexity 78

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 90.3%

Importance

Changes 0
Metric Value
wmc 78
lcom 1
cbo 3
dl 0
loc 405
ccs 149
cts 165
cp 0.903
rs 2.16
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
C handle() 0 48 15
C getHandlerType() 0 32 13
A isInvocableClass() 0 5 1
C handleAsInvocableClass() 0 62 12
B isControllerAction() 0 44 8
C handleAsControllerAction() 0 61 14
C handleAsCallable() 0 30 12
A handleUsingDi() 0 21 3

How to fix   Complexity   

Complex Class

Complex classes like RouteHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RouteHandler, and based on these observations, apply Extract Interface, too.

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 147
    public function handle(
34
        ?string $method,
35
        ?string $path,
36
        $action,
37
        array $arguments = [],
38
        ContainerInterface $di = null
39
    ) {
40 147
        $this->di = $di;
41
42 147
        if (is_null($action)) {
43 1
            return;
44
        }
45
46 146
        if (is_callable($action)) {
47 118
            if (is_array($action)
48 118
                && is_string($action[0])
49 118
                && class_exists($action[0])
50
            ) {
51 2
                $action[] = $arguments;
52 2
                return $this->handleAsControllerAction($action);
53
            }
54 116
            return $this->handleAsCallable($action, $arguments);
55
        }
56
57 28
        if (is_string($action) && class_exists($action)) {
58 24
            $callable = $this->isControllerAction($method, $path, $action);
59 23
            if ($callable) {
60 20
                return $this->handleAsControllerAction($callable);
61
            }
62
63 3
            $isinvocable = $this->isInvocableClass($action);
64 3
            if ($isinvocable) {
65 2
                return $this->handleAsInvocableClass($action);
66
            }
67
        }
68
69 5
        if ($di
70 5
            && is_array($action)
71 5
            && isset($action[0])
72 5
            && isset($action[1])
73 5
            && is_string($action[0])
74
        ) {
75
            // Try to load service from app/di injected container
76 3
            return $this->handleUsingDi($action, $arguments, $di);
77
        }
78
        
79 2
        throw new ConfigurationException("Handler for route does not seem to be a callable action.");
80
    }
81
82
83
84
    /**
85
     * Get  an informative string representing the handler type.
86
     *
87
     * @param string|array                 $action    base for the callable.
88
     * @param ContainerInjectableInterface $di        container with services.
89
     *
90
     * @return string as the type of handler.
91
     */
92 2
    public function getHandlerType(
93
        $action,
94
        ContainerInterface $di = null
95
    ) {
96 2
        if (is_null($action)) {
97 1
            return "null";
98
        }
99
100 2
        if (is_callable($action)) {
101 1
            return "callable";
102
        }
103
104 2
        if (is_string($action) && class_exists($action)) {
105 1
            $callable = $this->isControllerAction(null, null, $action);
106 1
            if ($callable) {
107 1
                return "controller";
108
            }
109
        }
110
111 2
        if ($di
112 2
            && is_array($action)
113 2
            && isset($action[0])
114 2
            && isset($action[1])
115 2
            && is_string($action[0])
116 2
            && $di->has($action[0])
117 2
            && is_callable([$di->get($action[0]), $action[1]])
118
        ) {
119 1
            return "di";
120
        }
121
122 1
        return "not found";
123
    }
124
125
126
127
    /**
128
     * Check if action is a class with the magic method __invoke.
129
     *
130
     * @param string $action the proposed handler.
131
     *
132
     * @return boolean true if class has implemented __invoke, else false.
133
     */
134 3
    protected function isInvocableClass(string $action) : bool
135
    {
136 3
        $rc = new \ReflectionClass($action);
137 3
        return $rc->hasMethod("__invoke");
138
    }
139
140
141
142
    /**
143
     * Call the __invoke action with optional arguments and call
144
     * initialisation methods if available.
145
     *
146
     * @param string $class as class that implements __invokable.
147
     *
148
     * @return mixed result from the handler.
149
     */
150 2
    protected function handleAsInvocableClass(string $class)
151
    {
152 2
        $obj = new $class();
153
        // $class = $callable[0];
154 2
        $action = "__invoke";
155
        // $args = $callable[2];
156
157 2
        $refl = new \ReflectionClass($class);
158 2
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
159 2
        $appInterface = "Anax\Commons\AppInjectableInterface";
160
161 2
        if ($this->di && $refl->implementsInterface($diInterface)) {
162 1
            $obj->setDI($this->di);
163 1
        } elseif ($this->di && $refl->implementsInterface($appInterface)) {
164
            if (!$this->di->has("app")) {
165
                throw new ConfigurationException(
166
                    "Controller '$class' implements AppInjectableInterface but \$app is not available in \$di."
167
                );
168
            }
169
            $obj->setApp($this->di->get("app"));
170
        }
171
172
        try {
173 2
            $refl = new \ReflectionMethod($class, "initialize");
174 1
            if ($refl->isPublic()) {
175 1
                $obj->initialize();
176
            }
177 1
        } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by Mikael Roos
Consider adding a comment why this CATCH block is empty.
Loading history...
178
            ;
179
        }
180
181 2
        $refl = new \ReflectionMethod($obj, $action);
182 2
        $paramIsVariadic = false;
0 ignored issues
show
Unused Code introduced by Mikael Roos
$paramIsVariadic is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
183 2
        foreach ($refl->getParameters() as $param) {
184
            if ($param->isVariadic()) {
185
                $paramIsVariadic = true;
0 ignored issues
show
Unused Code introduced by Mikael Roos
$paramIsVariadic is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
186
                break;
187
            }
188
        }
189
190
        // if (!$paramIsVariadic
191
        //     && $refl->getNumberOfParameters() < count($args)
192
        // ) {
193
        //     throw new NotFoundException(
194
        //         "Controller '$class' with action method '$action' valid but to many parameters. Got "
195
        //         . count($args)
196
        //         . ", expected "
197
        //         . $refl->getNumberOfParameters() . "."
198
        //     );
199
        // }
200
201
        try {
202
            //$res = $obj(...$args);
203 2
            $res = $obj();
204
        } catch (\ArgumentCountError $e) {
0 ignored issues
show
Bug introduced by Mikael Roos
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...
205
            throw new NotFoundException($e->getMessage());
206
        } catch (\TypeError $e) {
207
            throw new NotFoundException($e->getMessage());
208
        }
209
210 2
        return $res;
211
    }
212
213
214
215
    /**
216
     * Check if items can be used to call a controller action, verify
217
     * that the controller exists, the action has a class-method to call.
218
     *
219
     * @param string $method the request method.
220
     * @param string $path   the matched path, base for the controller action
221
     *                       and the arguments.
222
     * @param string $class  the controller class
223
     *
224
     * @return array with callable details.
225
     */
226 25
    protected function isControllerAction(
227
        ?string $method,
228
        ?string $path,
229
        string $class
230
    ) {
231 25
        $method = ucfirst(strtolower($method));
232 25
        $args = explode("/", $path);
233 25
        $action = array_shift($args);
234 25
        $action = empty($action) ? "index" : $action;
235 25
        $action = str_replace("-", "", $action);
236 25
        $action1 = "{$action}Action{$method}";
237 25
        $action2 = "{$action}Action";
238 25
        $action3 = "catchAll{$method}";
239 25
        $action4 = "catchAll";
240
241 25
        foreach ([$action1, $action2] as $target) {
242
            try {
243 25
                $refl = new \ReflectionMethod($class, $target);
244 18
                if (!$refl->isPublic()) {
245 1
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
246
                }
247
248 17
                return [$class, $target, $args];
249 16
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by Mikael Roos
Consider adding a comment why this CATCH block is empty.
Loading history...
250
                ;
251
            }
252
        }
253
254 7
        foreach ([$action3, $action4] as $target) {
255
            try {
256 7
                $refl = new \ReflectionMethod($class, $target);
257 4
                if (!$refl->isPublic()) {
258
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
259
                }
260
261 4
                array_unshift($args, $action);
262 4
                return [$class, $target, $args];
263 4
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by Mikael Roos
Consider adding a comment why this CATCH block is empty.
Loading history...
264
                ;
265
            }
266
        }
267
268 3
        return false;
269
    }
270
271
272
273
    /**
274
     * Call the controller action with optional arguments and call
275
     * initialisation methods if available.
276
     *
277
     * @param string $callable with details on what controller action to call.
278
     *
279
     * @return mixed result from the handler.
280
     */
281 22
    protected function handleAsControllerAction(array $callable)
282
    {
283 22
        $class = $callable[0];
284 22
        $action = $callable[1];
285 22
        $args = $callable[2];
286 22
        $obj = new $class();
287
288 22
        $refl = new \ReflectionClass($class);
289 22
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
290 22
        $appInterface = "Anax\Commons\AppInjectableInterface";
291
292 22
        if ($this->di && $refl->implementsInterface($diInterface)) {
293 1
            $obj->setDI($this->di);
294 21
        } elseif ($this->di && $refl->implementsInterface($appInterface)) {
295 2
            if (!$this->di->has("app")) {
296 1
                throw new ConfigurationException(
297 1
                    "Controller '$class' implements AppInjectableInterface but \$app is not available in \$di."
298
                );
299
            }
300 1
            $obj->setApp($this->di->get("app"));
301
        }
302
303
        try {
304 21
            $refl = new \ReflectionMethod($class, "initialize");
305 14
            if ($refl->isPublic()) {
306 14
                $obj->initialize();
307
            }
308 7
        } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by Mikael Roos
Consider adding a comment why this CATCH block is empty.
Loading history...
309
            ;
310
        }
311
312 21
        $refl = new \ReflectionMethod($obj, $action);
313 21
        $paramIsVariadic = false;
314 21
        foreach ($refl->getParameters() as $param) {
315 9
            if ($param->isVariadic()) {
316 4
                $paramIsVariadic = true;
317 9
                break;
318
            }
319
        }
320
321 21
        if (!$paramIsVariadic
322 21
            && $refl->getNumberOfParameters() < count($args)
323
        ) {
324 1
            throw new NotFoundException(
325 1
                "Controller '$class' with action method '$action' valid but to many parameters. Got "
326 1
                . count($args)
327 1
                . ", expected "
328 1
                . $refl->getNumberOfParameters() . "."
329
            );
330
        }
331
332
        try {
333 20
            $res = $obj->$action(...$args);
334 2
        } catch (\ArgumentCountError $e) {
0 ignored issues
show
Bug introduced by Mikael Roos
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...
335 1
            throw new NotFoundException($e->getMessage());
336 1
        } catch (\TypeError $e) {
337 1
            throw new NotFoundException($e->getMessage());
338
        }
339
340 18
        return $res;
341
    }
342
343
344
345
    /**
346
     * Handle as callable support callables where the method is not static.
347
     *
348
     * @param string|array                 $action    base for the callable
349
     * @param array                        $arguments optional arguments
350
     * @param ContainerInjectableInterface $di        container with services
0 ignored issues
show
Bug introduced by Mikael Roos
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...
351
     *
352
     * @return mixed as the result from the route handler.
353
     */
354 116
    protected function handleAsCallable(
355
        $action,
356
        array $arguments
357
    ) {
358 116
        if (is_array($action)
359 116
            && isset($action[0])
360 116
            && isset($action[1])
361 116
            && is_string($action[0])
362 116
            && is_string($action[1])
363 116
            && class_exists($action[0])
364
        ) {
365
            // ["SomeClass", "someMethod"] but not static
366
            $refl = new \ReflectionMethod($action[0], $action[1]);
367
            if ($refl->isPublic() && !$refl->isStatic()) {
368
                $obj = new $action[0]();
369
                return $obj->{$action[1]}(...$arguments);
370
            }
371
        }
372
373
        // Add $di to param list, if defined by the callback
374 116
        $refl = is_array($action)
375 1
            ? new \ReflectionMethod($action[0], $action[1])
376 116
            : new \ReflectionFunction($action);
377 116
        $params = $refl->getParameters();
378 116
        if (isset($params[0]) && $params[0]->getName() === "di") {
379 1
            array_unshift($arguments, $this->di);
380
        }
381
382 116
        return call_user_func($action, ...$arguments);
383
    }
384
385
386
387
    /**
388
     * Load callable as a service from the $di container.
389
     *
390
     * @param string|array                 $action    base for the callable
391
     * @param array                        $arguments optional arguments
392
     * @param ContainerInjectableInterface $di        container with services
393
     *
394
     * @return mixed as the result from the route handler.
395
     */
396 3
    protected function handleUsingDi(
397
        $action,
398
        array $arguments,
399
        ContainerInterface $di
400
    ) {
401 3
        if (!$di->has($action[0])) {
402 1
            throw new ConfigurationException("Routehandler '{$action[0]}' not loaded in di.");
403
        }
404
    
405 2
        $service = $di->get($action[0]);
406 2
        if (!is_callable([$service, $action[1]])) {
407 1
            throw new ConfigurationException(
408 1
                "Routehandler '{$action[0]}' does not have a callable method '{$action[1]}'."
409
            );
410
        }
411
    
412 1
        return call_user_func(
413 1
            [$service, $action[1]],
414 1
            ...$arguments
415
        );
416
    }
417
}
418