Test Failed
Pull Request — master (#19)
by Flo
02:43
created

Dispatcher::dispatch()   B

Complexity

Conditions 9
Paths 65

Size

Total Lines 56
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 56
rs 7.1584
c 0
b 0
f 0
cc 9
eloc 29
nc 65
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Class Dispatcher | Dispatcher.php
4
 * @package Faulancer\AbstractController
5
 * @author Florian Knapp <[email protected]>
6
 */
7
namespace Faulancer\Controller;
8
9
use Faulancer\Exception\ClassNotFoundException;
10
use Faulancer\Exception\DispatchFailureException;
11
use Faulancer\Exception\IncompatibleResponseException;
12
use Faulancer\Form\AbstractFormHandler;
13
use Faulancer\Http\JsonResponse;
14
use Faulancer\Http\Request;
15
use Faulancer\Http\Response;
16
use Faulancer\Exception\MethodNotFoundException;
17
use Faulancer\Service\AuthenticatorService;
18
use Faulancer\Service\Config;
19
use Faulancer\Service\SessionManagerService;
20
use Faulancer\ServiceLocator\ServiceLocator;
21
use Faulancer\Session\SessionManager;
22
23
/**
24
 * Class Dispatcher
25
 */
26
class Dispatcher
27
{
28
29
    /**
30
     * The current request object
31
     *
32
     * @var Request
33
     */
34
    protected $request;
35
36
    /**
37
     * The configuration object
38
     *
39
     * @var Config
40
     */
41
    protected $config;
42
43
    /**
44
     * user, api
45
     *
46
     * @var string
47
     */
48
    protected $requestType = 'default';
49
50
    /**
51
     * Dispatcher constructor.
52
     *
53
     * @param Request $request
54
     * @param Config  $config
55
     */
56
    public function __construct(Request $request, Config $config)
57
    {
58
        $this->request = $request;
59
        $this->config  = $config;
60
    }
61
62
    /**
63
     * Bootstrap for every route call
64
     *
65
     * @return Response|JsonResponse|mixed
66
     * @throws MethodNotFoundException
67
     * @throws ClassNotFoundException
68
     * @throws DispatchFailureException
69
     * @throws IncompatibleResponseException
70
     */
71
    public function dispatch()
72
    {
73
        // Check for core assets path
74
        if ($assets = $this->resolveAssetsPath()) {
75
            return $assets;
76
        }
77
78
        /** @var Response $response */
79
        $response = null;
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
80
81
        if ($this->detectLanguageSwitch()) {
82
            $serviceLocator = ServiceLocator::instance();
83
            $sessionManager = $serviceLocator->get(SessionManagerService::class);
84
            $sessionManager->set('language', $this->request->getParam('lang'));
85
        }
86
87
        if (strpos($this->request->getUri(), '/api') === 0) {
88
            $this->requestType = 'api';
89
        }
90
91
        $target  = $this->getRoute($this->request->getUri());
92
        $class   = $target['class'];
93
        $action  = $this->requestType === 'api' ? $this->getRestfulAction() : $target['action'];
94
        $payload = !empty($target['var']) ? $target['var'] : [];
95
96
        $payload = array_map('stripslashes', $payload);
97
        $payload = array_map('htmlentities', $payload);
98
        $payload = array_map('strip_tags', $payload);
99
100
        /** @var Response|AbstractController $class */
101
        $class = new $class($this->request);
102
103
        if (!method_exists($class, $action)) {
104
            throw new MethodNotFoundException('Class "' . get_class($class) . '" doesn\'t have the method ' . $action);
105
        }
106
107
        $authRequired = $this->detectPermissions($class, $action);
0 ignored issues
show
Bug introduced by
It seems like $class defined by new $class($this->request) on line 101 can also be of type object<Faulancer\Http\Response>; however, Faulancer\Controller\Dis...er::detectPermissions() does only seem to accept object<Faulancer\Controller\AbstractController>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
108
109
        if ($authRequired) {
110
111
            $permDeniedTemplate = $this->config->get('auth:authFailTemplate');
112
            $response           = $class->render($permDeniedTemplate);
0 ignored issues
show
Bug introduced by
The method render does only exist in Faulancer\Controller\AbstractController, but not in Faulancer\Http\Response.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
113
114
        } else {
115
116
            $response = call_user_func_array([$class, $action], $payload);
117
118
            if (!$response instanceof Response) {
119
                throw new IncompatibleResponseException('No valid response returned.');
120
            }
121
122
        }
123
124
        return $response;
125
126
    }
127
128
    private function detectLanguageSwitch()
129
    {
130
        return $this->request->getParam('lang') !== null;
131
    }
132
133
    /**
134
     * @param AbstractController $class
135
     * @param string             $action
136
     * @return bool|int
137
     */
138
    private function detectPermissions($class, $action)
139
    {
140
        if (method_exists($class, 'getRequiredPermissions') && empty($class->getRequiredPermissions())) {
141
            return false;
142
        }
143
144
        $class->$action();
145
146
        $permissions = $class->getRequiredPermissions();
147
148
        if (empty($permissions)) {
149
            return false;
150
        }
151
152
        /** @var AuthenticatorService $authenticator */
153
        $authenticator = ServiceLocator::instance()->get(AuthenticatorService::class);
154
155
        if (!empty($authenticator->getUserFromSession()) && !$authenticator->isAuthenticated($permissions)) {
156
            return true;
157
        } elseif (empty($authenticator->getUserFromSession())) {
158
            return $authenticator->redirectToAuthentication();
159
        }
160
161
        return false;
162
    }
163
164
    /**
165
     * @return bool|string
166
     */
167
    private function resolveAssetsPath()
168
    {
169
        $matches = [];
170
171
        if (preg_match('/(?<style>css)|(?<script>js)/', $this->request->getUri(), $matches)) {
172
173
            $file = $this->request->getUri();
174
175
            if (strpos($file, 'core') !== false) {
176
177
                $path = str_replace('/core', '', $file);
178
179
                if ($matches['style'] === 'css') {
180
                    return $this->sendCssFileHeader($path);
181
                } else if ($matches['script'] === 'js') {
182
                    return $this->sendJsFileHeader($path);
183
                }
184
185
            }
186
187
        }
188
189
        return false;
190
    }
191
192
    /**
193
     * @param $file
194
     * @return string
195
     * @codeCoverageIgnore
196
     */
197
    public function sendCssFileHeader($file)
198
    {
199
        header('Content-Type: text/css');
200
        echo file_get_contents(__DIR__ . '/../../public/assets' . $file);
201
        exit(0);
202
    }
203
204
    /**
205
     * @param $file
206
     * @return string
207
     * @codeCoverageIgnore
208
     */
209
    public function sendJsFileHeader($file)
210
    {
211
        header('Content-Type: text/javascript');
212
        echo file_get_contents(__DIR__ . '/../../public/assets' . $file);
213
        exit(0);
214
    }
215
216
    /**
217
     * Get data for specific route path
218
     *
219
     * @param string $path
220
     *
221
     * @return array
222
     * @throws MethodNotFoundException
223
     */
224
    private function getRoute($path)
225
    {
226
        if (strpos($this->request->getUri(), '/api') === 0) {
227
            $routes = $this->config->get('routes:rest');
228
        } else {
229
            $routes = $this->config->get('routes');
230
        }
231
232
        foreach ($routes as $name => $data) {
233
234
            if ($target = $this->getDirectMatch($path, $data)) {
235
                return $target;
236
            } else if ($target = $this->getVariableMatch($path, $data)) {
237
                return $target;
238
            }
239
240
        }
241
242
        throw new MethodNotFoundException('No matching route for path "' . $path . '" found');
243
    }
244
245
    /**
246
     * Determines if we have a direct/static route match
247
     *
248
     * @param string $uri  The request uri
249
     * @param array  $data The result from ClassParser
250
     *
251
     * @return array
252
     * @throws MethodNotFoundException
253
     */
254
    private function getDirectMatch($uri, array $data) :array
255
    {
256
        if (!empty($data['path']) && $uri === $data['path']) {
257
258
            if ($this->requestType === 'default' && in_array($this->request->getMethod(), $data['method'])) {
259
260
                return [
261
                    'class'  => $data['controller'],
262
                    'action' => $data['action'] . 'Action'
263
                ];
264
265 View Code Duplication
            } else if ($this->requestType === 'api') {
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...
266
267
                return [
268
                    'class'  => $data['controller'],
269
                    'action' => $this->getRestfulAction()
270
                ];
271
272
            }
273
274
            throw new MethodNotFoundException('Non valid request method available.');
275
276
        }
277
278
        return [];
279
    }
280
281
    /**
282
     * Determines if we have a variable route match
283
     *
284
     * @param string $uri
285
     * @param array  $data
286
     *
287
     * @return array
288
     * @throws MethodNotFoundException
289
     */
290
    private function getVariableMatch($uri, array $data) :array
291
    {
292
        if (empty($data['path']) || $data['path'] === '/') {
293
            return [];
294
        }
295
296
        $var   = [];
297
        $regex = str_replace(['/', '___'], ['\/', '+'], $data['path']);
298
299
        if (preg_match('|^' . $regex . '$|', $uri, $var)) {
300
301
            array_splice($var, 0, 1);
302
303
            if ($this->requestType === 'default'  && in_array($this->request->getMethod(), $data['method'])) {
304
305
                return [
306
                    'class'  => $data['controller'],
307
                    'action' => $data['action'] . 'Action',
308
                    'var'    => $var
309
                ];
310
311 View Code Duplication
            } else if ($this->requestType === 'api') {
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...
312
313
                return [
314
                    'class'  => $data['controller'],
315
                    'action' => $this->getRestfulAction(),
316
                    'var'    => $var
317
                ];
318
319
            }
320
321
        }
322
323
        return [];
324
    }
325
326
    /**
327
     * @return string
328
     */
329
    private function getRestfulAction()
330
    {
331
        $method = strtoupper($this->request->getMethod());
332
333
        switch ($method) {
334
335
            case 'GET':
336
                return 'get';
337
338
            case 'POST':
339
                return 'create';
340
341
            case 'PUT':
342
                return 'update';
343
344
            case 'DELETE':
345
                return 'delete';
346
347
            case 'PATCH':
348
                return 'update';
349
350
            default:
351
                return 'get';
352
353
        }
354
    }
355
356
}