Dispatcher::_getParamsFromVariableName()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 14
Code Lines 8

Duplication

Lines 3
Ratio 21.43 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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