RouteHandler   F
last analyzed

Complexity

Total Complexity 80

Size/Duplication

Total Lines 408
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 169
dl 0
loc 408
rs 2
c 0
b 0
f 0
wmc 80

8 Methods

Rating   Name   Duplication   Size   Complexity  
A handleUsingDi() 0 19 3
C handleAsCallable() 0 29 12
A isInvocableClass() 0 4 1
C handle() 0 47 15
B isControllerAction() 0 43 8
D handleAsControllerAction() 0 63 15
C handleAsInvocableClass() 0 64 13
C getHandlerType() 0 31 13

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.

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
    public function handle(
34
        ?string $method,
35
        ?string $path,
36
        $action,
37
        array $arguments = [],
38
        ContainerInterface $di = null
39
    ) {
40
        $this->di = $di;
41
42
        if (is_null($action)) {
0 ignored issues
show
introduced by
The condition is_null($action) is always false.
Loading history...
43
            return;
44
        }
45
46
        if (is_callable($action)) {
47
            if (is_array($action)
48
                && is_string($action[0])
49
                && class_exists($action[0])
50
            ) {
51
                $action[] = $arguments;
52
                return $this->handleAsControllerAction($action);
53
            }
54
            return $this->handleAsCallable($action, $arguments);
55
        }
56
57
        if (is_string($action) && class_exists($action)) {
58
            $callable = $this->isControllerAction($method, $path, $action);
59
            if ($callable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $callable of type array<integer,mixed|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$callable is a non-empty array, thus is always true.
Loading history...
60
                return $this->handleAsControllerAction($callable);
61
            }
62
63
            $isinvocable = $this->isInvocableClass($action);
64
            if ($isinvocable) {
65
                return $this->handleAsInvocableClass($action);
66
            }
67
        }
68
69
        if ($di
70
            && is_array($action)
71
            && isset($action[0])
72
            && isset($action[1])
73
            && is_string($action[0])
74
        ) {
75
            // Try to load service from app/di injected container
76
            return $this->handleUsingDi($action, $arguments, $di);
77
        }
78
79
        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
    public function getHandlerType(
93
        $action,
94
        ContainerInterface $di = null
95
    ) {
96
        if (is_null($action)) {
0 ignored issues
show
introduced by
The condition is_null($action) is always false.
Loading history...
97
            return "null";
98
        }
99
100
        if (is_callable($action)) {
101
            return "callable";
102
        }
103
104
        if (is_string($action) && class_exists($action)) {
105
            $callable = $this->isControllerAction(null, null, $action);
106
            if ($callable) {
0 ignored issues
show
introduced by
$callable is a non-empty array, thus is always true.
Loading history...
Bug Best Practice introduced by
The expression $callable of type array<integer,mixed|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
107
                return "controller";
108
            }
109
        }
110
111
        if ($di
112
            && is_array($action)
113
            && isset($action[0])
114
            && isset($action[1])
115
            && is_string($action[0])
116
            && $di->has($action[0])
117
            && is_callable([$di->get($action[0]), $action[1]])
118
        ) {
119
            return "di";
120
        }
121
122
        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
    protected function isInvocableClass(string $action) : bool
135
    {
136
        $rc = new \ReflectionClass($action);
137
        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
    protected function handleAsInvocableClass(string $class)
151
    {
152
        $obj = new $class();
153
        // $class = $callable[0];
154
        $action = "__invoke";
155
        // $args = $callable[2];
156
157
        $refl = new \ReflectionClass($class);
158
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
159
        $appInterface = "Anax\Commons\AppInjectableInterface";
160
161
        if ($this->di && $refl->implementsInterface($diInterface)) {
162
            $obj->setDI($this->di);
163
        } 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
            $refl = new \ReflectionMethod($class, "initialize");
174
            if ($refl->isPublic()) {
175
                $res = $obj->initialize();
176
                if (!is_null($res)) {
177
                    return $res;
178
                }
179
            }
180
        } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
181
            ;
182
        }
183
184
        $refl = new \ReflectionMethod($obj, $action);
185
        $paramIsVariadic = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $paramIsVariadic is dead and can be removed.
Loading history...
186
        foreach ($refl->getParameters() as $param) {
187
            if ($param->isVariadic()) {
188
                $paramIsVariadic = true;
189
                break;
190
            }
191
        }
192
193
        // if (!$paramIsVariadic
194
        //     && $refl->getNumberOfParameters() < count($args)
195
        // ) {
196
        //     throw new NotFoundException(
197
        //         "Controller '$class' with action method '$action' valid but to many parameters. Got "
198
        //         . count($args)
199
        //         . ", expected "
200
        //         . $refl->getNumberOfParameters() . "."
201
        //     );
202
        // }
203
204
        try {
205
            //$res = $obj(...$args);
206
            $res = $obj();
207
        } catch (\ArgumentCountError $e) {
208
            throw new NotFoundException($e->getMessage());
209
        } catch (\TypeError $e) {
210
            throw new NotFoundException($e->getMessage());
211
        }
212
213
        return $res;
214
    }
215
216
217
218
    /**
219
     * Check if items can be used to call a controller action, verify
220
     * that the controller exists, the action has a class-method to call.
221
     *
222
     * @param string $method the request method.
223
     * @param string $path   the matched path, base for the controller action
224
     *                       and the arguments.
225
     * @param string $class  the controller class
226
     *
227
     * @return array with callable details.
228
     */
229
    protected function isControllerAction(
230
        ?string $method,
231
        ?string $path,
232
        string $class
233
    ) {
234
        $method = ucfirst(strtolower($method));
0 ignored issues
show
Bug introduced by
It seems like $method can also be of type null; however, parameter $string of strtolower() 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

234
        $method = ucfirst(strtolower(/** @scrutinizer ignore-type */ $method));
Loading history...
235
        $args = explode("/", $path);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $string of explode() 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

235
        $args = explode("/", /** @scrutinizer ignore-type */ $path);
Loading history...
236
        $action = array_shift($args);
237
        $action = empty($action) ? "index" : $action;
238
        $action = str_replace("-", "", $action);
239
        $action1 = "{$action}Action{$method}";
240
        $action2 = "{$action}Action";
241
        $action3 = "catchAll{$method}";
242
        $action4 = "catchAll";
243
244
        foreach ([$action1, $action2] as $target) {
245
            try {
246
                $refl = new \ReflectionMethod($class, $target);
247
                if (!$refl->isPublic()) {
248
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
249
                }
250
251
                return [$class, $target, $args];
252
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
253
                ;
254
            }
255
        }
256
257
        foreach ([$action3, $action4] as $target) {
258
            try {
259
                $refl = new \ReflectionMethod($class, $target);
260
                if (!$refl->isPublic()) {
261
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
262
                }
263
264
                array_unshift($args, $action);
265
                return [$class, $target, $args];
266
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
267
                ;
268
            }
269
        }
270
271
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
272
    }
273
274
275
276
    /**
277
     * Call the controller action with optional arguments and call
278
     * initialisation methods if available.
279
     *
280
     * @param string $callable with details on what controller action to call.
281
     *
282
     * @return mixed result from the handler.
283
     */
284
    protected function handleAsControllerAction(array $callable)
285
    {
286
        $class = $callable[0];
287
        $action = $callable[1];
288
        $args = $callable[2];
289
        $obj = new $class();
290
291
        $refl = new \ReflectionClass($class);
292
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
293
        $appInterface = "Anax\Commons\AppInjectableInterface";
294
295
        if ($this->di && $refl->implementsInterface($diInterface)) {
296
            $obj->setDI($this->di);
297
        } elseif ($this->di && $refl->implementsInterface($appInterface)) {
298
            if (!$this->di->has("app")) {
299
                throw new ConfigurationException(
300
                    "Controller '$class' implements AppInjectableInterface but \$app is not available in \$di."
301
                );
302
            }
303
            $obj->setApp($this->di->get("app"));
304
        }
305
306
        try {
307
            $refl = new \ReflectionMethod($class, "initialize");
308
            if ($refl->isPublic()) {
309
                $res = $obj->initialize();
310
                if (!is_null($res)) {
311
                    return $res;
312
                }
313
            }
314
        } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
315
            ;
316
        }
317
318
        $refl = new \ReflectionMethod($obj, $action);
319
        $paramIsVariadic = false;
320
        foreach ($refl->getParameters() as $param) {
321
            if ($param->isVariadic()) {
322
                $paramIsVariadic = true;
323
                break;
324
            }
325
        }
326
327
        if (!$paramIsVariadic
328
            && $refl->getNumberOfParameters() < count($args)
329
        ) {
330
            throw new NotFoundException(
331
                "Controller '$class' with action method '$action' valid but to many parameters. Got "
332
                . count($args)
333
                . ", expected "
334
                . $refl->getNumberOfParameters() . "."
335
            );
336
        }
337
338
        try {
339
            $res = $obj->$action(...$args);
340
        } catch (\ArgumentCountError $e) {
341
            throw new NotFoundException($e->getMessage());
342
        } catch (\TypeError $e) {
343
            throw new NotFoundException($e->getMessage());
344
        }
345
346
        return $res;
347
    }
348
349
350
351
    /**
352
     * Handle as callable support callables where the method is not static.
353
     *
354
     * @param string|array                 $action    base for the callable
355
     * @param array                        $arguments optional arguments
356
     * @param ContainerInjectableInterface $di        container with services
357
     *
358
     * @return mixed as the result from the route handler.
359
     */
360
    protected function handleAsCallable(
361
        $action,
362
        array $arguments
363
    ) {
364
        if (is_array($action)
365
            && isset($action[0])
366
            && isset($action[1])
367
            && is_string($action[0])
368
            && is_string($action[1])
369
            && class_exists($action[0])
370
        ) {
371
            // ["SomeClass", "someMethod"] but not static
372
            $refl = new \ReflectionMethod($action[0], $action[1]);
373
            if ($refl->isPublic() && !$refl->isStatic()) {
374
                $obj = new $action[0]();
375
                return $obj->{$action[1]}(...$arguments);
376
            }
377
        }
378
379
        // Add $di to param list, if defined by the callback
380
        $refl = is_array($action)
381
            ? new \ReflectionMethod($action[0], $action[1])
382
            : new \ReflectionFunction($action);
383
        $params = $refl->getParameters();
384
        if (isset($params[0]) && $params[0]->getName() === "di") {
385
            array_unshift($arguments, $this->di);
386
        }
387
388
        return call_user_func($action, ...$arguments);
389
    }
390
391
392
393
    /**
394
     * Load callable as a service from the $di container.
395
     *
396
     * @param string|array                 $action    base for the callable
397
     * @param array                        $arguments optional arguments
398
     * @param ContainerInjectableInterface $di        container with services
399
     *
400
     * @return mixed as the result from the route handler.
401
     */
402
    protected function handleUsingDi(
403
        $action,
404
        array $arguments,
405
        ContainerInterface $di
406
    ) {
407
        if (!$di->has($action[0])) {
408
            throw new ConfigurationException("Routehandler '{$action[0]}' not loaded in di.");
409
        }
410
411
        $service = $di->get($action[0]);
412
        if (!is_callable([$service, $action[1]])) {
413
            throw new ConfigurationException(
414
                "Routehandler '{$action[0]}' does not have a callable method '{$action[1]}'."
415
            );
416
        }
417
418
        return call_user_func(
419
            [$service, $action[1]],
420
            ...$arguments
421
        );
422
    }
423
}
424