Completed
Push — master ( 66292f...50f6f9 )
by Mikael
02:01
created

RouteHandler::isControllerAction()   B

Complexity

Conditions 8
Paths 50

Size

Total Lines 44

Duplication

Lines 25
Ratio 56.82 %

Code Coverage

Tests 23
CRAP Score 8.0046

Importance

Changes 0
Metric Value
dl 25
loc 44
ccs 23
cts 24
cp 0.9583
rs 7.9715
c 0
b 0
f 0
cc 8
nc 50
nop 3
crap 8.0046
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
            return $this->handleAsCallable($action, $arguments);
48
        }
49
50 24
        if (is_string($action) && class_exists($action)) {
51 20
            $callable = $this->isControllerAction($method, $path, $action);
52 19
            if ($callable) {
53 18
                return $this->handleAsControllerAction($callable);
54
            }
55
        }
56
57 5
        if ($di
58 5
            && is_array($action)
59 5
            && isset($action[0])
60 5
            && isset($action[1])
61 5
            && is_string($action[0])
62
        ) {
63
            // Try to load service from app/di injected container
64 3
            return $this->handleUsingDi($action, $arguments, $di);
65
        }
66
        
67 2
        throw new ConfigurationException("Handler for route does not seem to be a callable action.");
68
    }
69
70
71
72
    /**
73
     * Get  an informative string representing the handler type.
74
     *
75
     * @param string|array                 $action    base for the callable.
76
     * @param ContainerInjectableInterface $di        container with services.
77
     *
78
     * @return string as the type of handler.
79
     */
80 2
    public function getHandlerType(
81
        $action,
82
        ContainerInterface $di = null
83
    ) {
84 2
        if (is_null($action)) {
85 1
            return "null";
86
        }
87
88 2
        if (is_callable($action)) {
89 1
            return "callable";
90
        }
91
92 2
        if (is_string($action) && class_exists($action)) {
93 1
            $callable = $this->isControllerAction(null, null, $action);
94 1
            if ($callable) {
95 1
                return "controller";
96
            }
97
        }
98
99 2
        if ($di
100 2
            && is_array($action)
101 2
            && isset($action[0])
102 2
            && isset($action[1])
103 2
            && is_string($action[0])
104 2
            && $di->has($action[0])
105 2
            && is_callable([$di->get($action[0]), $action[1]])
106
        ) {
107 1
            return "di";
108
        }
109
110 1
        return "not found";
111
    }
112
113
114
115
    /**
116
     * Check if items can be used to call a controller action, verify
117
     * that the controller exists, the action has a class-method to call.
118
     *
119
     * @param string $method the request method.
120
     * @param string $path   the matched path, base for the controller action
121
     *                       and the arguments.
122
     * @param string $class  the controller class
123
     *
124
     * @return array with callable details.
125
     */
126 21
    protected function isControllerAction(
127
        string $method = null,
128
        string $path = null,
129
        string $class
130
    ) {
131 21
        $method = ucfirst(strtolower($method));
132 21
        $args = explode("/", $path);
133 21
        $action = array_shift($args);
134 21
        $action = empty($action) ? "index" : $action;
135 21
        $action = str_replace("-", "", $action);
136 21
        $action1 = "{$action}Action{$method}";
137 21
        $action2 = "{$action}Action";
138 21
        $action3 = "catchAll{$method}";
139 21
        $action4 = "catchAll";
140
141 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...
142
            try {
143 21
                $refl = new \ReflectionMethod($class, $target);
144 16
                if (!$refl->isPublic()) {
145 1
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
146
                }
147
148 15
                return [$class, $target, $args];
149 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...
150
                ;
151
            }
152
        }
153
154 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...
155
            try {
156 5
                $refl = new \ReflectionMethod($class, $target);
157 4
                if (!$refl->isPublic()) {
158
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
159
                }
160
161 4
                array_unshift($args, $action);
162 4
                return [$class, $target, $args];
163 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...
164
                ;
165
            }
166
        }
167
168 1
        return false;
169
    }
170
171
172
173
    /**
174
     * Call the controller action with optional arguments and call
175
     * initialisation methods if available.
176
     *
177
     * @param string $callable with details on what controller action to call.
178
     *
179
     * @return mixed result from the handler.
180
     */
181 18
    protected function handleAsControllerAction(array $callable)
182
    {
183 18
        $class = $callable[0];
184 18
        $action = $callable[1];
185 18
        $args = $callable[2];
186 18
        $obj = new $class();
187
188 18
        $refl = new \ReflectionClass($class);
189 18
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
190 18
        $appInterface = "Anax\Commons\AppInjectableInterface";
191
192 18
        if ($this->di && $refl->implementsInterface($diInterface)) {
193 1
            $obj->setDI($this->di);
194 17
        } elseif ($this->di && $refl->implementsInterface($appInterface)) {
195 2
            if (!$this->di->has("app")) {
196 1
                throw new ConfigurationException(
197 1
                    "Controller '$class' implements AppInjectableInterface but \$app is not available in \$di."
198
                );
199
            }
200 1
            $obj->setApp($this->di->get("app"));
201
        }
202
203
        try {
204 17
            $refl = new \ReflectionMethod($class, "initialize");
205 12
            if ($refl->isPublic()) {
206 12
                $obj->initialize();
207
            }
208 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...
209
            ;
210
        }
211
212 17
        $refl = new \ReflectionMethod($obj, $action);
213 17
        $paramIsVariadic = false;
214 17
        foreach ($refl->getParameters() as $param) {
215 9
            if ($param->isVariadic()) {
216 4
                $paramIsVariadic = true;
217 9
                break;
218
            }
219
        }
220
221 17
        if (!$paramIsVariadic
222 17
            && $refl->getNumberOfParameters() < count($args)
223
        ) {
224 1
            throw new NotFoundException(
225 1
                "Controller '$class' with action method '$action' valid but to many parameters. Got "
226 1
                . count($args)
227 1
                . ", expected "
228 1
                . $refl->getNumberOfParameters() . "."
229
            );
230
        }
231
232
        try {
233 16
            $res = $obj->$action(...$args);
234 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...
235 1
            throw new NotFoundException($e->getMessage());
236 1
        } catch (\TypeError $e) {
237 1
            throw new NotFoundException($e->getMessage());
238
        }
239
240 14
        return $res;
241
    }
242
243
244
245
    /**
246
     * Handle as callable support callables where the method is not static.
247
     *
248
     * @param string|array                 $action    base for the callable
249
     * @param array                        $arguments optional arguments
250
     * @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...
251
     *
252
     * @return mixed as the result from the route handler.
253
     */
254 117
    protected function handleAsCallable(
255
        $action,
256
        array $arguments
257
    ) {
258 117
        if (is_array($action)
259 117
            && isset($action[0])
260 117
            && isset($action[1])
261 117
            && is_string($action[0])
262 117
            && is_string($action[1])
263 117
            && class_exists($action[0])
264
        ) {
265
            // ["SomeClass", "someMethod"] but not static
266 2
            $refl = new \ReflectionMethod($action[0], $action[1]);
267 2
            if ($refl->isPublic() && !$refl->isStatic()) {
268 1
                $obj = new $action[0]();
269 1
                return $obj->{$action[1]}(...$arguments);
270
            }
271
        }
272
273
        // Add $di to param list, if defined by the callback
274 116
        $refl = is_array($action)
275 2
            ? new \ReflectionMethod($action[0], $action[1])
276 116
            : new \ReflectionFunction($action);
277 116
        $params = $refl->getParameters();
278 116
        if (isset($params[0]) && $params[0]->getName() === "di") {
279 1
            array_unshift($arguments, $this->di);
280
        }
281
282 116
        return call_user_func($action, ...$arguments);
283
    }
284
285
286
287
    /**
288
     * Load callable as a service from the $di container.
289
     *
290
     * @param string|array                 $action    base for the callable
291
     * @param array                        $arguments optional arguments
292
     * @param ContainerInjectableInterface $di        container with services
293
     *
294
     * @return mixed as the result from the route handler.
295
     */
296 3
    protected function handleUsingDi(
297
        $action,
298
        array $arguments,
299
        ContainerInterface $di
300
    ) {
301 3
        if (!$di->has($action[0])) {
302 1
            throw new ConfigurationException("Routehandler '{$action[0]}' not loaded in di.");
303
        }
304
    
305 2
        $service = $di->get($action[0]);
306 2
        if (!is_callable([$service, $action[1]])) {
307 1
            throw new ConfigurationException(
308 1
                "Routehandler '{$action[0]}' does not have a callable method '{$action[1]}'."
309
            );
310
        }
311
    
312 1
        return call_user_func(
313 1
            [$service, $action[1]],
314 1
            ...$arguments
315
        );
316
    }
317
}
318