Completed
Push — master ( 1b32c6...be6f90 )
by Mikael
02:20
created

src/Route/RouteHandler.php (4 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 143
    public function handle(
34
        string $method = null,
35
        string $path = null,
36
        $action,
37
        array $arguments = [],
38
        ContainerInterface $di = null
39
    ) {
40 143
        $this->di = $di;
41
42 143
        if (is_null($action)) {
43 1
            return;
44
        }
45
46 142
        if (is_callable($action)) {
47 117
            return $this->handleAsCallable($action, $arguments);
48
        }
49
50 25
        if (is_string($action) && class_exists($action)) {
51 21
            $callable = $this->isControllerAction($method, $path, $action);
52 20
            if ($callable) {
53 19
                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 22
    protected function isControllerAction(
127
        string $method = null,
128
        string $path = null,
129
        string $class
130
    ) {
131 22
        $args = explode("/", $path);
132 22
        $action = array_shift($args);
133 22
        $action = empty($action) ? "index" : $action;
134 22
        $action = str_replace("-", "", $action);
135 22
        $action1 = "{$action}Action" . ucfirst(strtolower($method));
136 22
        $action2 = "{$action}Action";
137 22
        $action3 = "catchAll";
138
139 22
        $refl = null;
140 22
        foreach ([$action1, $action2, $action3] as $action) {
141
            try {
142 22
                $refl = new \ReflectionMethod($class, $action);
143 21
                if (!$refl->isPublic()) {
144 1
                    throw new NotFoundException("Controller method '$class::$action' is not a public method.");
145
                }
146
147 20
                return [$class, $action, $args];
148 19
            } catch (\ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
149
                ;
150
            }
151
        }
152
153 1
        return false;
154
    }
155
156
157
158
    /**
159
     * Call the controller action with optional arguments and call
160
     * initialisation methods if available.
161
     *
162
     * @param string $callable with details on what controller action to call.
163
     *
164
     * @return mixed result from the handler.
165
     */
166 19
    protected function handleAsControllerAction(array $callable)
167
    {
168 19
        $class = $callable[0];
169 19
        $action = $callable[1];
170 19
        $args = $callable[2];
171 19
        $obj = new $class();
172
173 19
        $refl = new \ReflectionClass($class);
174 19
        if ($this->di && $refl->implementsInterface("Anax\Commons\ContainerInjectableInterface")) {
175 1
            $obj->setDI($this->di);
176
        }
177
178
        try {
179 19
            $refl = new \ReflectionMethod($class, "initialize");
180 17
            if ($refl->isPublic()) {
181 17
                $obj->initialize();
182
            }
183 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...
184
            ;
185
        }
186
187 19
        $refl = new \ReflectionMethod($obj, $action);
188 19
        $paramIsVariadic = false;
189 19
        foreach ($refl->getParameters() as $param) {
190 8
            if ($param->isVariadic()) {
191 3
                $paramIsVariadic = true;
192 8
                break;
193
            }
194
        }
195
196 19
        if (!$paramIsVariadic
197 19
            && $refl->getNumberOfParameters() < count($args)
198
        ) {
199 1
            throw new NotFoundException(
200 1
                "Controller '$class' with action method '$action' valid but to many parameters. Got "
201 1
                . count($args)
202 1
                . ", expected "
203 1
                . $refl->getNumberOfParameters() . "."
204
            );
205
        }
206
207
        try {
208 18
            $res = $obj->$action(...$args);
209 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...
210 1
            throw new NotFoundException($e->getMessage());
211 1
        } catch (\TypeError $e) {
212 1
            throw new NotFoundException($e->getMessage());
213
        }
214
215 16
        return $res;
216
    }
217
218
219
220
    /**
221
     * Handle as callable support callables where the method is not static.
222
     *
223
     * @param string|array                 $action    base for the callable
224
     * @param array                        $arguments optional arguments
225
     * @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...
226
     *
227
     * @return mixed as the result from the route handler.
228
     */
229 117
    protected function handleAsCallable(
230
        $action,
231
        array $arguments
232
    ) {
233 117
        if (is_array($action)
234 117
            && isset($action[0])
235 117
            && isset($action[1])
236 117
            && is_string($action[0])
237 117
            && is_string($action[1])
238 117
            && class_exists($action[0])
239
        ) {
240
            // ["SomeClass", "someMethod"] but not static
241 2
            $refl = new \ReflectionMethod($action[0], $action[1]);
242 2
            if ($refl->isPublic() && !$refl->isStatic()) {
243 1
                $obj = new $action[0]();
244 1
                return $obj->{$action[1]}(...$arguments);
245
            }
246
        }
247
248
        // Add $di to param list, if defined by the callback
249 116
        $refl = is_array($action)
250 2
            ? new \ReflectionMethod($action[0], $action[1])
251 116
            : new \ReflectionFunction($action);
252 116
        $params = $refl->getParameters();
253 116
        if (isset($params[0]) && $params[0]->getName() === "di") {
254 1
            array_unshift($arguments, $this->di);
255
        }
256
257 116
        return call_user_func($action, ...$arguments);
258
    }
259
260
261
262
    /**
263
     * Load callable as a service from the $di container.
264
     *
265
     * @param string|array                 $action    base for the callable
266
     * @param array                        $arguments optional arguments
267
     * @param ContainerInjectableInterface $di        container with services
268
     *
269
     * @return mixed as the result from the route handler.
270
     */
271 3
    protected function handleUsingDi(
272
        $action,
273
        array $arguments,
274
        ContainerInterface $di
275
    ) {
276 3
        if (!$di->has($action[0])) {
277 1
            throw new ConfigurationException("Routehandler '{$action[0]}' not loaded in di.");
278
        }
279
    
280 2
        $service = $di->get($action[0]);
281 2
        if (!is_callable([$service, $action[1]])) {
282 1
            throw new ConfigurationException(
283 1
                "Routehandler '{$action[0]}' does not have a callable method '{$action[1]}'."
284
            );
285
        }
286
    
287 1
        return call_user_func(
288 1
            [$service, $action[1]],
289 1
            ...$arguments
290
        );
291
    }
292
}
293