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