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

src/Route/RouteHandler.php (3 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) {
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) {
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) {
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
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