Test Failed
Push — master ( 0ee32e...41c938 )
by Dan
07:10
created

Dispatcher::dispatch()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
cc 5
eloc 19
nc 7
nop 3
1
<?php
2
/**
3
 * This file is part of the PSR Http 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 Psr\Http\Message\ResponseInterface;
13
use Psr\Http\Message\ServerRequestInterface;
14
use Ds\Router\Exceptions\DispatchException;
15
use Zend\Diactoros\Response;
16
use Zend\Diactoros\Stream;
17
18
/**
19
 * Class Dispatcher
20
 *
21
 * @package Ds\Router\Dispatcher
22
 * @author  Dan Smith    <[email protected]>
23
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
24
 * @link    https://github.com/djsmithme/Router
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
    public function __construct(array $options = array())
63
    {
64
        $options = \array_replace_recursive(self::$defaults, $options);
65
        $this->options = $options;
66
    }
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
        switch ($handlerType) {
82
            case 'string':
83
                $resolvedHandler = $this->_processStringHandler($handler);
84
                $this->_getParams($resolvedHandler['controller'], $constructor);
85
                $response = $this->_callController($request, $resolvedHandler['controller'], $resolvedHandler['method']);
86
                break;
87
            case 'object':
88
                if (\is_callable($handler)) {
89
                    $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 101 which is incompatible with the return type documented by Ds\Router\Dispatcher\Dispatcher::dispatch of type Psr\Http\Message\ResponseInterface.
Loading history...
90
                }
91
                break;
92
            default:
93
                throw new DispatchException(
94
                    "Handler type: {$handlerType} is not valid."
95
                );
96
        }
97
98
        if (!$response instanceof \Psr\Http\Message\ResponseInterface) {
99
            $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 101 which is incompatible with the return type documented by Ds\Router\Dispatcher\Dispatcher::dispatch of type Psr\Http\Message\ResponseInterface.
Loading history...
100
        }
101
        return $response;
102
    }
103
104
    /***
105
     * Get Controller/Method from handler string.
106
     *
107
     * @param $handler
108
     * @return mixed
109
     * @throws DispatchException
110
     */
111
    protected function _processStringHandler($handler)
112
    {
113
        $controllerMethod = \explode(
114
            $this->options['stringHandlerPattern'], $handler
115
        );
116
117
        try {
118
            $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...
119
        } catch (\Exception $e) {
120
            throw new DispatchException($e->getMessage());
121
        }
122
123
        $resolvedHandler['method'] = $controllerMethod[1] ?? null;
124
125
        return $resolvedHandler;
126
    }
127
128
    /**
129
     * Find class from namespace.
130
     *
131
     * @param string $class
132
     * @return string
133
     * @throws DispatchException
134
     */
135
    protected function _findClass($class)
136
    {
137
        if (\class_exists($class)) {
138
            return $class;
139
        }
140
141
        foreach ($this->namespaces as $namespace) {
142
            if (\class_exists($namespace . $class)) {
143
                return $namespace . $class;
144
            }
145
        }
146
147
        throw new DispatchException("Controller : {$class} does not exist");
148
    }
149
150
    /**
151
     * Find class construct parameters from provided arguments.
152
     *
153
     * @param string $controllerName
154
     * @param array $args
155
     * @return array
156
     * @throws DispatchException
157
     */
158
    protected function _getParams(string $controllerName, array $args)
159
    {
160
        if ($this->options['autoWire'] === false) {
161
            $this->providedArguments = $args;
162
            return $this->providedArguments;
163
        }
164
165
        $this->providedArguments = [];
166
        $this->constructParams = $this->_getConstructParams($controllerName);
167
168
        try {
169
            $this->_getParamsFromVariableName($args);
170
        } catch (\Exception $e) {
171
            $this->_getParamsFromTypeHint($args);
172
        }
173
174 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...
175
            throw new DispatchException('Missing parameters');
176
        }
177
178
        \ksort($this->providedArguments);
179
        return $this->providedArguments;
180
    }
181
182
    /**
183
     * Reflect Controller construct and get parameters.
184
     *
185
     * @param $controllerName
186
     * @internal
187
     *
188
     * @return \ReflectionParameter[]
189
     */
190
    protected function _getConstructParams($controllerName)
191
    {
192
        $controllerReflection = new \ReflectionClass($controllerName);
193
        $constructor = $controllerReflection->getConstructor();
194
195
        if (null === $constructor) {
196
            return [];
197
        }
198
199
        return $constructor->getParameters();
200
    }
201
202
    /**
203
     * Find controller constructor params based on $arg[index]
204
     *
205
     * @param $args
206
     * @throws DispatchException
207
     */
208
    protected function _getParamsFromVariableName($args)
209
    {
210
        foreach ($this->constructParams as $index => $param) {
211
            $parameterVarName = $param->getName();
212
            $constructorParamIndex = (int)$param->getPosition();
213
214
            if (isset($args[$parameterVarName])) {
215
                $this->providedArguments[$constructorParamIndex] = $args[$parameterVarName];
216
            }
217
        }
218 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...
219
            throw new DispatchException('Missing parameters');
220
        }
221
    }
222
223
    /**
224
     * Find missing parameters based on Class name or type hint.
225
     *
226
     * @param $args
227
     */
228
    protected function _getParamsFromTypeHint($args)
229
    {
230
        $processedArgs = $this->_processConstructArgs($args);
231
232
        foreach ($this->constructParams as $index => $param) {
233
234
            /**
235
             * @var \ReflectionClass $constructorParamClass ;
236
             */
237
            $constructorParamClass = $param->getClass();
238
            $constructorParamIndex = (int)$param->getPosition();
239
240
            if (isset($this->providedArguments[$constructorParamIndex])) {
241
                continue;
242
            }
243
244
            if ($constructorParamClass) {
245
                $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...
246
                foreach ((array)$processedArgs['object'] as $useParam) {
247
                    $key = \array_search($constructParamTypeHint, $useParam['interfaces'], true);
248
                    if ($key !== false) {
249
                        $this->providedArguments[$constructorParamIndex] = $useParam['value'];
250
                    }
251
                }
252
            }
253
        }
254
    }
255
256
    /**
257
     * Process provided arguments.
258
     *
259
     * @param array $args
260
     * @return array
261
     */
262
    protected function _processConstructArgs(array $args = [])
263
    {
264
        $arguments = [];
265
        foreach ($args as $index => $param) {
266
            switch (\gettype($param)) {
267
                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...
268
                    $arguments['object'][] = [
269
                        'class' => \get_class($param),
270
                        'interfaces' => \class_implements($param),
271
                        'value' => $param
272
                    ];
273
                    break;
274
            }
275
        }
276
        return $arguments;
277
    }
278
279
    /**
280
     * Call controller with provided request and return Response.
281
     *
282
     * @param ServerRequestInterface $request     Server Request
283
     * @param string                 $controller  Controller class.
284
     * @param string                 $method      Controller method.
285
     *
286
     * @return ResponseInterface
287
     * @throws DispatchException
288
     */
289
    protected function _callController(ServerRequestInterface $request, string $controller, string $method)
290
    {
291
        $controllerObj = new $controller(...$this->providedArguments);
292
293
        try {
294
            $this->_checkWhiteList($controller, $this->options);
295
            $this->_checkBlackList($controller, $this->options);
296
        } catch (\Exception $e) {
297
            unset($controllerObj);
298
            throw new DispatchException($e->getMessage());
299
        }
300
301
        if (!method_exists($controllerObj,$method)){
302
            throw new DispatchException('Controller method does not exist');
303
        }
304
305
        return $controllerObj->{$method}($request);
306
    }
307
308
    /**
309
     * Checks controller instance against whitelist.
310
     *
311
     * @param $controller
312
     * @param $options
313
     * @throws DispatchException
314
     */
315 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...
316
    {
317
        if (isset($options['blacklist'])) {
318
            if (!$this->_checkInList($controller, $options['blacklist'])) {
319
                throw new DispatchException("Controller: {$controller}, not part of allowed whitelist.");
320
            }
321
        }
322
    }
323
324
    /**
325
     * Internal function for checking controller instance against class list.
326
     *
327
     * @param $controller
328
     * @param array $classList
329
     * @internal
330
     * @return bool
331
     */
332
    protected function _checkInList($controller, array $classList)
333
    {
334
        foreach ((array)$classList as $classes) {
335
            if ($controller instanceof $classes) {
336
                return true;
337
            }
338
        }
339
        return false;
340
    }
341
342
    /**
343
     * Checks controller instance against blacklist.
344
     *
345
     * @param $controller
346
     * @param $options
347
     * @throws DispatchException
348
     */
349 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...
350
    {
351
        if (isset($options['blacklist'])) {
352
            if ($this->_checkInList($controller, $options['blacklist'])) {
353
                throw new DispatchException("Controller: {$controller}, found on blacklist.");
354
            }
355
        }
356
    }
357
358
    /**
359
     * Create a new PSR-7 Response.
360
     *
361
     * @param $controllerResponse
362
     * @return Response
363
     * @throws \InvalidArgumentException
364
     */
365
    public function createNewResponse($controllerResponse)
366
    {
367
        $response = new Response();
368
        $body = new Stream("php://memory", "wb+");
369
        $body->write($controllerResponse);
370
        $body->rewind();
371
        return $response->withBody($body);
372
    }
373
374
    /**
375
     * With Class Namespace.
376
     *
377
     * @param $namespace
378
     * @return Dispatcher
379
     */
380
    public function withNamespace($namespace)
381
    {
382
        $new = clone $this;
383
        $new->namespaces[$namespace] = $namespace;
384
        return $new;
385
    }
386
387
    /**
388
     * With class Namespaces.
389
     *
390
     * @param array $namespaces
391
     * @return Dispatcher
392
     */
393
    public function withNamespaces(array $namespaces)
394
    {
395
        $new = clone $this;
396
        foreach ($namespaces as $ns) {
397
            $new->namespaces[$ns] = $ns;
398
        }
399
        return $new;
400
    }
401
}
402