Completed
Pull Request — master (#19)
by Flo
02:37
created

Dispatcher::run()   B

Complexity

Conditions 4
Paths 9

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 16
nc 9
nop 0
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\Event\Observer;
10
use Faulancer\Event\Type\OnDispatch;
11
use Faulancer\Exception\ClassNotFoundException;
12
use Faulancer\Exception\DispatchFailureException;
13
use Faulancer\Exception\IncompatibleResponseException;
14
use Faulancer\Http\JsonResponse;
15
use Faulancer\Http\Request;
16
use Faulancer\Http\Response;
17
use Faulancer\Exception\MethodNotFoundException;
18
use Faulancer\Service\AuthenticatorPlugin;
19
use Faulancer\Service\AuthenticatorService;
20
use Faulancer\Service\Config;
21
use Faulancer\Service\SessionManagerService;
22
use Faulancer\ServiceLocator\ServiceLocator;
23
24
/**
25
 * Class Dispatcher
26
 */
27
class Dispatcher
28
{
29
30
    /**
31
     * The current request object
32
     *
33
     * @var Request
34
     */
35
    protected $request;
36
37
    /**
38
     * The configuration object
39
     *
40
     * @var Config
41
     */
42
    protected $config;
43
44
    /**
45
     * user, api
46
     *
47
     * @var string
48
     */
49
    protected $requestType = 'default';
50
51
    /**
52
     * Dispatcher constructor.
53
     *
54
     * @param Request $request
55
     * @param Config  $config
56
     */
57
    public function __construct(Request $request, Config $config)
58
    {
59
        $this->request = $request;
60
        $this->config  = $config;
61
    }
62
63
    /**
64
     * Bootstrap for every route call
65
     *
66
     * @return Response|JsonResponse|mixed
67
     * @throws MethodNotFoundException
68
     * @throws ClassNotFoundException
69
     * @throws DispatchFailureException
70
     * @throws IncompatibleResponseException
71
     */
72
    public function dispatch()
73
    {
74
        // Check for core assets path
75
        if ($assets = $this->_resolveAssetsPath()) {
76
            return $assets;
77
        }
78
79
        Observer::instance()->trigger(new OnDispatch($this));
80
81
        $this->_setLanguageFromUri();
82
83
        if (strpos($this->request->getPath(), '/api') === 0) {
84
            $this->requestType = 'api';
85
        }
86
87
        list($class, $action, $permission, $payload) = $this->_getRoute($this->request->getPath());
88
89
        /** @var AbstractController $class */
90
        $class   = new $class($this->request);
91
92
        if (!empty($permission)) {
93
94
            /** @var AuthenticatorService $authenticator */
95
            $authenticator = ServiceLocator::instance()->get(AuthenticatorService::class);
96
            $isPermitted   = $authenticator->isPermitted($permission);
97
98
            if ($isPermitted === null) {
99
100
                $class->redirect($this->config->get('auth:authUrl'));
101
102
            } else if ($isPermitted === false) {
103
104
                $errorController = $this->config->get('customErrorController');
105
                return (new $errorController($this->request))->notPermittedAction();
106
107
            }
108
109
        }
110
111
        if (!method_exists($class, $action)) {
112
113
            throw new MethodNotFoundException(
114
                'Class "' . get_class($class) . '" doesn\'t have the method ' . $action
115
            );
116
117
        }
118
119
        $payload = array_map('strip_tags', $payload);
120
        $payload = array_map('htmlspecialchars', $payload);
121
122
        $response = call_user_func_array([$class, $action], $payload);
123
124
        if (!$response instanceof Response) {
125
            throw new IncompatibleResponseException('No valid response returned.');
126
        }
127
128
        return $response;
129
130
    }
131
132
    /**
133
     * @return bool
134
     */
135
    private function _setLanguageFromUri()
136
    {
137
        if ($this->request->getParam('lang') !== null) {
138
139
            $serviceLocator = ServiceLocator::instance();
140
141
            /** @var SessionManagerService $sessionManager */
142
            $sessionManager = $serviceLocator->get(SessionManagerService::class);
143
            $sessionManager->set('language', $this->request->getParam('lang'));
144
145
            return true;
146
147
        }
148
149
        return false;
150
    }
151
152
    /**
153
     * @return bool|string
154
     */
155
    private function _resolveAssetsPath()
156
    {
157
        $matches = [];
158
159
        if (preg_match('/(?<style>css)|(?<script>js)/', $this->request->getPath(), $matches)) {
160
161
            $file = $this->request->getPath();
162
163
            if (strpos($file, 'core') !== false) {
164
165
                $path = str_replace('/core', '', $file);
166
167
                if ($matches['style'] === 'css') {
168
                    return $this->sendCssFileHeader($path);
169
                } else if ($matches['script'] === 'js') {
170
                    return $this->sendJsFileHeader($path);
171
                }
172
173
            }
174
175
        }
176
177
        return false;
178
    }
179
180
    /**
181
     * @param $file
182
     * @return string
183
     * @codeCoverageIgnore
184
     */
185
    public function sendCssFileHeader($file)
186
    {
187
        header('Content-Type: text/css');
188
        echo file_get_contents(__DIR__ . '/../../public/assets' . $file);
189
        exit(0);
190
    }
191
192
    /**
193
     * @param $file
194
     * @return string
195
     * @codeCoverageIgnore
196
     */
197
    public function sendJsFileHeader($file)
198
    {
199
        header('Content-Type: text/javascript');
200
        echo file_get_contents(__DIR__ . '/../../public/assets' . $file);
201
        exit(0);
202
    }
203
204
    /**
205
     * Get data for specific route path
206
     *
207
     * @param string $path
208
     *
209
     * @return array
210
     * @throws MethodNotFoundException
211
     */
212
    private function _getRoute($path)
213
    {
214
        if (strpos($this->request->getPath(), '/api') === 0) {
215
            $routes = $this->config->get('routes:rest');
216
        } else {
217
            $routes = $this->config->get('routes');
218
        }
219
220
        foreach ($routes as $name => $data) {
221
222
            if ($target = $this->_getDirectMatch($path, $data)) {
223
                return $target;
224
            } else if ($target = $this->_getVariableMatch($path, $data)) {
225
                return $target;
226
            }
227
228
        }
229
230
        throw new MethodNotFoundException('No matching route for path "' . $path . '" found');
231
    }
232
233
    /**
234
     * Determines if we have a direct/static route match
235
     *
236
     * @param string $uri  The request uri
237
     * @param array  $data The result from ClassParser
238
     *
239
     * @return array
240
     * @throws MethodNotFoundException
241
     */
242
    private function _getDirectMatch($uri, array $data) :array
243
    {
244
        if (!empty($data['path']) && $uri === $data['path']) {
245
246 View Code Duplication
            if ($this->requestType === 'default' && in_array($this->request->getMethod(), $data['method'])) {
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...
247
248
                return [
249
                    $data['controller'],
250
                    $data['action'] . 'Action',
251
                    $data['permission'] ?? null,
252
                    []
253
                ];
254
255
            } else if ($this->requestType === 'api') {
256
257
                return [
258
                    $data['controller'],
259
                    $this->_getRestfulAction(),
260
                    $data['permission'] ?? null,
261
                    []
262
                ];
263
264
            }
265
266
            throw new MethodNotFoundException('Non valid request method available.');
267
268
        }
269
270
        return [];
271
    }
272
273
    /**
274
     * Determines if we have a variable route match
275
     *
276
     * @param string $uri
277
     * @param array  $data
278
     *
279
     * @return array
280
     * @throws MethodNotFoundException
281
     */
282
    private function _getVariableMatch($uri, array $data) :array
283
    {
284
        if (empty($data['path']) || $data['path'] === '/') {
285
            return [];
286
        }
287
288
        $var   = [];
289
        $regex = str_replace(['/', '___'], ['\/', '+'], $data['path']);
290
291
        if (preg_match('|^' . $regex . '$|', $uri, $var)) {
292
293
            array_splice($var, 0, 1);
294
295 View Code Duplication
            if ($this->requestType === 'default'  && in_array($this->request->getMethod(), $data['method'])) {
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...
296
297
                return [
298
                    $data['controller'],
299
                    $data['action'] . 'Action',
300
                    $data['permission'] ?? null,
301
                    $var
302
                ];
303
304
            } else if ($this->requestType === 'api') {
305
306
                return [
307
                    $data['controller'],
308
                    $this->_getRestfulAction(),
309
                    $data['permission'] ?? null,
310
                    $var
311
                ];
312
313
            }
314
315
        }
316
317
        return [];
318
    }
319
    
320
321
    /**
322
     * @return string
323
     */
324
    private function _getRestfulAction()
325
    {
326
        $method = strtoupper($this->request->getMethod());
327
328
        switch ($method) {
329
330
            case 'GET':
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...
331
                return 'get';
332
333
            case 'POST':
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...
334
                return 'create';
335
336
            case 'PUT':
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...
337
                return 'update';
338
339
            case 'DELETE':
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...
340
                return 'delete';
341
342
            case 'PATCH':
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...
343
                return 'update';
344
345
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
346
                return 'get';
347
348
        }
349
    }
350
351
}