Test Failed
Push — master ( 3e96d2...d4b731 )
by Dan
06:30
created

Dispatcher::dispatch()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 0
cts 17
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 23
nc 9
nop 3
crap 42
1
<?php
2
/**
3
 * This file is part of the RS Framework.
4
 *
5
 * (c) Dan Smith <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Ds\Router\Dispatcher;
11
12
use Ds\Router\Exceptions\DispatchException;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\ServerRequestInterface;
15
use Zend\Diactoros\Response;
16
use Zend\Diactoros\Stream;
17
18
/**
19
 * Class Dispatcher
20
 *
21
 * @package Rs\Router\Dispatcher
22
 * @author  Dan Smith    <[email protected]>
23
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
24
 * @link    https://red-sqr.co.uk/Framework/Router/wikis/
25
 */
26
class Dispatcher
27
{
28
    /**
29
     * Dispatcher default options.
30
     *
31
     * @var array
32
     */
33
    protected static $defaults = [
34
        'stringHandlerPattern' => '::',
35
        'autoWire' => true
36
    ];
37
    /**
38
     * @var array
39
     */
40
    private $options;
41
    /**
42
     * @var array
43
     */
44
    private $providedArguments;
45
    /**
46
     * @var array
47
     */
48
    private $constructParams;
49
50
    /**
51
     * Handler Class Namespaces.
52
     *
53
     * @var array.
54
     */
55
    private $namespaces = [];
56
57
    /**
58
     * Dispatcher constructor.
59
     *
60
     * @param array $options
61
     */
62 2
    public function __construct(array $options = array())
63
    {
64 2
        $options = \array_replace_recursive(self::$defaults, $options);
65 2
        $this->options = $options;
66 2
    }
67
68
    /**
69
     * Get response from handler string.
70
     *
71
     * @param $request
72
     * @param string|\Closure $handler
73
     * @param array $constructor
74
     * @return ResponseInterface
75
     * @throws \Exception
76
     */
77
    public function dispatch(ServerRequestInterface $request, $handler, array $constructor = array())
78
    {
79
        $handlerType = \gettype($handler);
80
        $response = null;
81
82
        switch ($handlerType) {
83
            case 'string':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
84
85
                $params = \explode($this->options['stringHandlerPattern'], $handler);
86
                if (!isset($params[1])){
87
                    $response = $handler;
88
                    break;
89
                }
90
91
                $resolvedHandler = $this->_processStringHandler($handler);
92
                $this->_getParams($resolvedHandler['controller'], $constructor);
93
                $response = $this->_callController($request, $resolvedHandler['controller'], $resolvedHandler['method']);
94
                break;
95
            case 'object':
96
                if (\is_callable($handler)) {
97
                    $response = $handler($request);
0 ignored issues
show
Bug Compatibility introduced by
The expression $handler($request); of type ? adds the type Zend\Diactoros\Response to the return on line 109 which is incompatible with the return type documented by Ds\Router\Dispatcher\Dispatcher::dispatch of type Psr\Http\Message\ResponseInterface.
Loading history...
98
                }
99
                break;
100
            default:
101
                throw new DispatchException(
102
                    "Handler type: {$handlerType} is not valid."
103
                );
104
        }
105
106
        if (!$response instanceof ResponseInterface) {
107
            $response = $this->createNewResponse($response);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->createNewResponse($response); of type Zend\Diactoros\Response adds the type Zend\Diactoros\Response to the return on line 109 which is incompatible with the return type documented by Ds\Router\Dispatcher\Dispatcher::dispatch of type Psr\Http\Message\ResponseInterface.
Loading history...
108
        }
109
        return $response;
110
    }
111
112
    /***
113
     * Get Controller/Method from handler string.
114
     *
115
     * @param $handler
116
     * @return mixed
117
     * @throws DispatchException
118
     */
119
    protected function _processStringHandler($handler)
120
    {
121
        $controllerMethod = \explode(
122
            $this->options['stringHandlerPattern'], $handler
123
        );
124
125
        try {
126
            $resolvedHandler['controller'] = $this->_findClass($controllerMethod[0]);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$resolvedHandler was never initialized. Although not strictly required by PHP, it is generally a good practice to add $resolvedHandler = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
127
        } catch (\Exception $e) {
128
            throw new DispatchException($e->getMessage());
129
        }
130
131
        $resolvedHandler['method'] = $controllerMethod[1] ?? null;
132
133
        return $resolvedHandler;
134
    }
135
136
    /**
137
     * Find class from namespace.
138
     *
139
     * @param string $class
140
     * @return string
141
     * @throws DispatchException
142
     */
143
    protected function _findClass($class)
144
    {
145
        if (\class_exists($class)) {
146
            return $class;
147
        }
148
149
        foreach ($this->namespaces as $namespace) {
150
            if (\class_exists($namespace . $class)) {
151
                return $namespace . $class;
152
            }
153
        }
154
155
        throw new DispatchException("Controller : {$class} does not exist");
156
    }
157
158
    /**
159
     * Find class construct parameters from provided arguments.
160
     *
161
     * @param string $controllerName
162
     * @param array $args
163
     * @return array
164
     * @throws DispatchException
165
     */
166
    protected function _getParams(string $controllerName, array $args)
167
    {
168
        if ($this->options['autoWire'] === false) {
169
            $this->providedArguments = $args;
170
            return $this->providedArguments;
171
        }
172
173
        $this->providedArguments = [];
174
        $this->constructParams = $this->_getConstructParams($controllerName);
175
176
        try {
177
            $this->_getParamsFromVariableName($args);
178
        } catch (\Exception $e) {
179
            $this->_getParamsFromTypeHint($args);
180
        }
181
182 View Code Duplication
        if (\count($this->providedArguments) - \count($this->constructParams) > 0) {
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...
183
            throw new DispatchException('Missing parameters');
184
        }
185
186
        \ksort($this->providedArguments);
187
        return $this->providedArguments;
188
    }
189
190
    /**
191
     * Reflect Controller construct and get parameters.
192
     *
193
     * @param $controllerName
194
     * @internal
195
     *
196
     * @return \ReflectionParameter[]
197
     */
198
    protected function _getConstructParams($controllerName)
199
    {
200
        $controllerReflection = new \ReflectionClass($controllerName);
201
        $constructor = $controllerReflection->getConstructor();
202
203
        if (null === $constructor) {
204
            return [];
205
        }
206
207
        return $constructor->getParameters();
208
    }
209
210
    /**
211
     * Find controller constructor params based on $arg[index]
212
     *
213
     * @param $args
214
     * @throws DispatchException
215
     */
216
    protected function _getParamsFromVariableName($args)
217
    {
218
        foreach ($this->constructParams as $index => $param) {
219
            $parameterVarName = $param->getName();
220
            $constructorParamIndex = (int)$param->getPosition();
221
222
            if (isset($args[$parameterVarName])) {
223
                $this->providedArguments[$constructorParamIndex] = $args[$parameterVarName];
224
            }
225
        }
226 View Code Duplication
        if (\count($this->constructParams) - \count($this->providedArguments) > 0) {
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...
227
            throw new DispatchException('Missing parameters');
228
        }
229
    }
230
231
    /**
232
     * Find missing parameters based on Class name or type hint.
233
     *
234
     * @param $args
235
     */
236
    protected function _getParamsFromTypeHint($args)
237
    {
238
        $processedArgs = $this->_processConstructArgs($args);
239
240
        foreach ($this->constructParams as $index => $param) {
241
242
            /**
243
             * @var \ReflectionClass $constructorParamClass ;
244
             */
245
            $constructorParamClass = $param->getClass();
246
            $constructorParamIndex = (int)$param->getPosition();
247
248
            if (isset($this->providedArguments[$constructorParamIndex])) {
249
                continue;
250
            }
251
252
            if ($constructorParamClass) {
253
                $constructParamTypeHint = $constructorParamClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $constructorParamClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
254
                foreach ((array)$processedArgs['object'] as $useParam) {
255
                    $key = \array_search($constructParamTypeHint, $useParam['interfaces'], true);
256
                    if ($key !== false) {
257
                        $this->providedArguments[$constructorParamIndex] = $useParam['value'];
258
                    }
259
                }
260
            }
261
        }
262
    }
263
264
    /**
265
     * Process provided arguments.
266
     *
267
     * @param array $args
268
     * @return array
269
     */
270
    protected function _processConstructArgs(array $args = [])
271
    {
272
        $arguments = [];
273
        foreach ($args as $index => $param) {
274
            switch (\gettype($param)) {
275
                case 'object':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
276
                    $arguments['object'][] = [
277
                        'class' => \get_class($param),
278
                        'interfaces' => \class_implements($param),
279
                        'value' => $param
280
                    ];
281
                    break;
282
            }
283
        }
284
        return $arguments;
285
    }
286
287
    /**
288
     * Call controller with provided request and return Response.
289
     *
290
     * @param ServerRequestInterface $request     Server Request
291
     * @param string                 $controller  Controller class.
292
     * @param string                 $method      Controller method.
293
     *
294
     * @return ResponseInterface
295
     * @throws DispatchException
296
     */
297
    protected function _callController(ServerRequestInterface $request, string $controller, string $method)
298
    {
299
300
        if (!class_exists($controller)){
301
            throw new DispatchException('Unable to locate controller: '.$controller);
302
        }
303
304
        $controllerObj = new $controller(...$this->providedArguments);
305
306
        try {
307
            $this->_checkWhiteList($controller, $this->options);
308
            $this->_checkBlackList($controller, $this->options);
309
        } catch (\Exception $e) {
310
            unset($controllerObj);
311
            throw new DispatchException($e->getMessage());
312
        }
313
314
        if (!method_exists($controllerObj,$method)){
315
            throw new DispatchException('Unable to locate method: '.$controller.'::'.$method);
316
        }
317
318
        return $controllerObj->{$method}($request);
319
    }
320
321
    /**
322
     * Checks controller instance against whitelist.
323
     *
324
     * @param $controller
325
     * @param $options
326
     * @throws DispatchException
327
     */
328 View Code Duplication
    protected function _checkWhiteList($controller, $options)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
329
    {
330
        if (isset($options['blacklist'])) {
331
            if (!$this->_checkInList($controller, $options['blacklist'])) {
332
                throw new DispatchException("Controller: {$controller}, not part of allowed whitelist.");
333
            }
334
        }
335
    }
336
337
    /**
338
     * Internal function for checking controller instance against class list.
339
     *
340
     * @param $controller
341
     * @param array $classList
342
     * @internal
343
     * @return bool
344
     */
345
    protected function _checkInList($controller, array $classList)
346
    {
347
        foreach ((array)$classList as $classes) {
348
            if ($controller instanceof $classes) {
349
                return true;
350
            }
351
        }
352
        return false;
353
    }
354
355
    /**
356
     * Checks controller instance against blacklist.
357
     *
358
     * @param $controller
359
     * @param $options
360
     * @throws DispatchException
361
     */
362 View Code Duplication
    protected function _checkBlackList($controller, $options)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
363
    {
364
        if (isset($options['blacklist'])) {
365
            if ($this->_checkInList($controller, $options['blacklist'])) {
366
                throw new DispatchException("Controller: {$controller}, found on blacklist.");
367
            }
368
        }
369
    }
370
371
    /**
372
     * Create a new PSR-7 Response.
373
     *
374
     * @param $controllerResponse
375
     * @return Response
376
     * @throws \InvalidArgumentException
377
     */
378
    public function createNewResponse($controllerResponse)
379
    {
380 1
        $response = new Response();
381
        $body = new Stream("php://memory", "wb+");
382 1
        $body->write($controllerResponse);
383 1
        $body->rewind();
384 1
        return $response->withBody($body);
385
    }
386
387
    /**
388
     * With Class Namespace.
389
     *
390
     * @param $namespace
391
     * @return Dispatcher
392
     */
393 1
    public function withNamespace($namespace)
394
    {
395 1
        $new = clone $this;
396 1
        $new->namespaces[$namespace] = $namespace;
397 1
        return $new;
398
    }
399 1
400
    /**
401
     * With class Namespaces.
402
     *
403
     * @param array $namespaces
404
     * @return Dispatcher
405
     */
406
    public function withNamespaces(array $namespaces)
407
    {
408
        $new = clone $this;
409
        foreach ($namespaces as $ns) {
410
            $new->namespaces[$ns] = $ns;
411
        }
412
        return $new;
413
    }
414
}
415