RouteHandler::isInvocableClass()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 149
    public function handle(
34
        ?string $method,
35
        ?string $path,
36
        $action,
37
        array $arguments = [],
38
        ContainerInterface $di = null
39
    ) {
40 149
        $this->di = $di;
41
42 149
        if (is_null($action)) {
43 1
            return;
44
        }
45
46 148
        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 30
        if (is_string($action) && class_exists($action)) {
58 26
            $callable = $this->isControllerAction($method, $path, $action);
59 25
            if ($callable) {
60 21
                return $this->handleAsControllerAction($callable);
61
            }
62
63 4
            $isinvocable = $this->isInvocableClass($action);
64 4
            if ($isinvocable) {
65 3
                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 4
    protected function isInvocableClass(string $action) : bool
135
    {
136 4
        $rc = new \ReflectionClass($action);
137 4
        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 3
    protected function handleAsInvocableClass(string $class)
151
    {
152 3
        $obj = new $class();
153
        // $class = $callable[0];
154 3
        $action = "__invoke";
155
        // $args = $callable[2];
156
157 3
        $refl = new \ReflectionClass($class);
158 3
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
159 3
        $appInterface = "Anax\Commons\AppInjectableInterface";
160
161 3
        if ($this->di && $refl->implementsInterface($diInterface)) {
162 1
            $obj->setDI($this->di);
163 2
        } 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 3
            $refl = new \ReflectionMethod($class, "initialize");
174 2
            if ($refl->isPublic()) {
175 2
                $res = $obj->initialize();
176 2
                if (!is_null($res)) {
177 2
                    return $res;
178
                }
179
            }
180 1
        } 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 2
        $refl = new \ReflectionMethod($obj, $action);
185 2
        $paramIsVariadic = false;
0 ignored issues
show
Unused Code introduced by
$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 2
        foreach ($refl->getParameters() as $param) {
187
            if ($param->isVariadic()) {
188
                $paramIsVariadic = true;
0 ignored issues
show
Unused Code introduced by
$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...
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 2
            $res = $obj();
207
        } 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...
208
            throw new NotFoundException($e->getMessage());
209
        } catch (\TypeError $e) {
210
            throw new NotFoundException($e->getMessage());
211
        }
212
213 2
        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 27
    protected function isControllerAction(
230
        ?string $method,
231
        ?string $path,
232
        string $class
233
    ) {
234 27
        $method = ucfirst(strtolower($method));
235 27
        $args = explode("/", $path);
236 27
        $action = array_shift($args);
237 27
        $action = empty($action) ? "index" : $action;
238 27
        $action = str_replace("-", "", $action);
239 27
        $action1 = "{$action}Action{$method}";
240 27
        $action2 = "{$action}Action";
241 27
        $action3 = "catchAll{$method}";
242 27
        $action4 = "catchAll";
243
244 27
        foreach ([$action1, $action2] as $target) {
245
            try {
246 27
                $refl = new \ReflectionMethod($class, $target);
247 19
                if (!$refl->isPublic()) {
248 1
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
249
                }
250
251 18
                return [$class, $target, $args];
252 17
            } 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 8
        foreach ([$action3, $action4] as $target) {
258
            try {
259 8
                $refl = new \ReflectionMethod($class, $target);
260 4
                if (!$refl->isPublic()) {
261
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
262
                }
263
264 4
                array_unshift($args, $action);
265 4
                return [$class, $target, $args];
266 5
            } 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 4
        return false;
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 23
    protected function handleAsControllerAction(array $callable)
285
    {
286 23
        $class = $callable[0];
287 23
        $action = $callable[1];
288 23
        $args = $callable[2];
289 23
        $obj = new $class();
290
291 23
        $refl = new \ReflectionClass($class);
292 23
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
293 23
        $appInterface = "Anax\Commons\AppInjectableInterface";
294
295 23
        if ($this->di && $refl->implementsInterface($diInterface)) {
296 1
            $obj->setDI($this->di);
297 22
        } elseif ($this->di && $refl->implementsInterface($appInterface)) {
298 2
            if (!$this->di->has("app")) {
299 1
                throw new ConfigurationException(
300 1
                    "Controller '$class' implements AppInjectableInterface but \$app is not available in \$di."
301
                );
302
            }
303 1
            $obj->setApp($this->di->get("app"));
304
        }
305
306
        try {
307 22
            $refl = new \ReflectionMethod($class, "initialize");
308 15
            if ($refl->isPublic()) {
309 15
                $res = $obj->initialize();
310 15
                if (!is_null($res)) {
311 15
                    return $res;
312
                }
313
            }
314 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...
315
            ;
316
        }
317
318 21
        $refl = new \ReflectionMethod($obj, $action);
319 21
        $paramIsVariadic = false;
320 21
        foreach ($refl->getParameters() as $param) {
321 9
            if ($param->isVariadic()) {
322 4
                $paramIsVariadic = true;
323 9
                break;
324
            }
325
        }
326
327 21
        if (!$paramIsVariadic
328 21
            && $refl->getNumberOfParameters() < count($args)
329
        ) {
330 1
            throw new NotFoundException(
331 1
                "Controller '$class' with action method '$action' valid but to many parameters. Got "
332 1
                . count($args)
333 1
                . ", expected "
334 1
                . $refl->getNumberOfParameters() . "."
335
            );
336
        }
337
338
        try {
339 20
            $res = $obj->$action(...$args);
340 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...
341 1
            throw new NotFoundException($e->getMessage());
342 1
        } catch (\TypeError $e) {
343 1
            throw new NotFoundException($e->getMessage());
344
        }
345
346 18
        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
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...
357
     *
358
     * @return mixed as the result from the route handler.
359
     */
360 116
    protected function handleAsCallable(
361
        $action,
362
        array $arguments
363
    ) {
364 116
        if (is_array($action)
365 116
            && isset($action[0])
366 116
            && isset($action[1])
367 116
            && is_string($action[0])
368 116
            && is_string($action[1])
369 116
            && 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 116
        $refl = is_array($action)
381 1
            ? new \ReflectionMethod($action[0], $action[1])
382 116
            : new \ReflectionFunction($action);
383 116
        $params = $refl->getParameters();
384 116
        if (isset($params[0]) && $params[0]->getName() === "di") {
385 1
            array_unshift($arguments, $this->di);
386
        }
387
388 116
        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 3
    protected function handleUsingDi(
403
        $action,
404
        array $arguments,
405
        ContainerInterface $di
406
    ) {
407 3
        if (!$di->has($action[0])) {
408 1
            throw new ConfigurationException("Routehandler '{$action[0]}' not loaded in di.");
409
        }
410
411 2
        $service = $di->get($action[0]);
412 2
        if (!is_callable([$service, $action[1]])) {
413 1
            throw new ConfigurationException(
414 1
                "Routehandler '{$action[0]}' does not have a callable method '{$action[1]}'."
415
            );
416
        }
417
418 1
        return call_user_func(
419 1
            [$service, $action[1]],
420 1
            ...$arguments
421
        );
422
    }
423
}
424