Completed
Push — master ( 799419...2ea7db )
by Mikael
04:18 queued 02:23
created

src/Route/RouteHandler.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 = null,
35
        string $path = null,
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
64 8
        if ($di
65 8
            && is_array($action)
66 8
            && isset($action[0])
67 8
            && isset($action[1])
68 8
            && 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 5
        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 25
    protected function isControllerAction(
134
        string $method = null,
135
        string $path = null,
136
        string $class
137
    ) {
138 25
        $method = ucfirst(strtolower($method));
139 25
        $args = explode("/", $path);
140 25
        $action = array_shift($args);
141 25
        $action = empty($action) ? "index" : $action;
142 25
        $action = str_replace("-", "", $action);
143 25
        $action1 = "{$action}Action{$method}";
144 25
        $action2 = "{$action}Action";
145 25
        $action3 = "catchAll{$method}";
146 25
        $action4 = "catchAll";
147
148 25 View Code Duplication
        foreach ([$action1, $action2] as $target) {
0 ignored issues
show
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 25
                $refl = new \ReflectionMethod($class, $target);
151 18
                if (!$refl->isPublic()) {
152 1
                    throw new NotFoundException("Controller method '$class::$target' is not a public method.");
153
                }
154
155 17
                return [$class, $target, $args];
156 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...
157
                ;
158
            }
159
        }
160
161 8 View Code Duplication
        foreach ([$action3, $action4] as $target) {
0 ignored issues
show
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 8
                $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 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...
171
                ;
172
            }
173
        }
174
175 4
        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 22
    protected function handleAsControllerAction(array $callable)
189
    {
190 22
        $class = $callable[0];
191 22
        $action = $callable[1];
192 22
        $args = $callable[2];
193 22
        $obj = new $class();
194
195 22
        $refl = new \ReflectionClass($class);
196 22
        $diInterface = "Anax\Commons\ContainerInjectableInterface";
197 22
        $appInterface = "Anax\Commons\AppInjectableInterface";
198
199 22
        if ($this->di && $refl->implementsInterface($diInterface)) {
200 1
            $obj->setDI($this->di);
201 21
        } 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 21
            $refl = new \ReflectionMethod($class, "initialize");
212 14
            if ($refl->isPublic()) {
213 14
                $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 21
        $refl = new \ReflectionMethod($obj, $action);
220 21
        $paramIsVariadic = false;
221 21
        foreach ($refl->getParameters() as $param) {
222 9
            if ($param->isVariadic()) {
223 4
                $paramIsVariadic = true;
224 9
                break;
225
            }
226
        }
227
228 21
        if (!$paramIsVariadic
229 21
            && $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 20
            $res = $obj->$action(...$args);
241 2
        } catch (\ArgumentCountError $e) {
0 ignored issues
show
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 18
        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
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 116
    protected function handleAsCallable(
262
        $action,
263
        array $arguments
264
    ) {
265 116
        if (is_array($action)
266 116
            && isset($action[0])
267 116
            && isset($action[1])
268 116
            && is_string($action[0])
269 116
            && is_string($action[1])
270 116
            && 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 116
        $refl = is_array($action)
282 1
            ? new \ReflectionMethod($action[0], $action[1])
283 116
            : new \ReflectionFunction($action);
284 116
        $params = $refl->getParameters();
285 116
        if (isset($params[0]) && $params[0]->getName() === "di") {
286 1
            array_unshift($arguments, $this->di);
287
        }
288
289 116
        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