Completed
Push — master ( 3f135d...7bfd57 )
by Alex
07:35
created

CommonApplication   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 3
Metric Value
eloc 107
dl 0
loc 373
rs 8.64
c 3
b 0
f 3
wmc 47

20 Methods

Rating   Name   Duplication   Size   Complexity  
A handleException() 0 5 1
A baseFormatter() 0 12 3
A result() 0 8 3
A getActionMessage() 0 15 3
A getActionMessageCode() 0 3 1
A setSuccessMessage() 0 3 1
A setTemplate() 0 3 1
A setGetVar() 0 4 2
A getTemplate() 0 3 1
A noRouteFoundErrorHandler() 0 3 1
A handleRestException() 0 7 1
B run() 0 26 7
A __construct() 0 12 1
A crossRender() 0 3 1
A loadActionsFromDirectory() 0 8 5
A getActionBuilderMethod() 0 9 3
A constructOverrideHandler() 0 9 2
A constructOtherView() 0 9 2
B createActionFromJsonConfig() 0 45 7
A loadActoinsFromConfig() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like CommonApplication often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CommonApplication, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Mezon\Application;
3
4
use Mezon\HtmlTemplate\HtmlTemplate;
5
use Mezon\Rest;
6
use Mezon\Router\Utils;
7
8
/**
9
 * Class CommonApplication
10
 *
11
 * @package Mezon
12
 * @subpackage CommonApplication
13
 * @author Dodonov A.A.
14
 * @version v.1.0 (2019/08/07)
15
 * @copyright Copyright (c) 2019, aeon.org
16
 */
17
18
/**
19
 * Common application with any available template
20
 *
21
 * To load routes from the config call $this->load_routes_from_config('./conf/routes.json');
22
 *
23
 * The format of the *.json config must be like this:
24
 *
25
 * [
26
 * {
27
 * "route" : "/route1" ,
28
 * "callback" : "callback1" ,
29
 * "method" : "POST"
30
 * } ,
31
 * {
32
 * "route" : "/route2" ,
33
 * "callback" : "callback2" ,
34
 * "method" : ["GET" , "POST"]
35
 * }
36
 * ]
37
 */
38
class CommonApplication extends Application
39
{
40
41
    /**
42
     * Application's template
43
     *
44
     * @var HtmlTemplate
45
     */
46
    private $template = false;
47
48
    /**
49
     * Constructor
50
     *
51
     * @param HtmlTemplate $template
52
     *            Template
53
     */
54
    public function __construct(HtmlTemplate $template)
55
    {
56
        parent::__construct();
57
58
        $this->template = $template;
59
60
        $this->getRouter()->setNoProcessorFoundErrorHandler([
61
            $this,
62
            'noRouteFoundErrorHandler'
63
        ]);
64
65
        $this->loadActoinsFromConfig();
66
    }
67
68
    /**
69
     * Method handles 404 errors
70
     *
71
     * @codeCoverageIgnore
72
     */
73
    public function noRouteFoundErrorHandler(): void
74
    {
75
        $this->redirectTo('/404');
76
    }
77
78
    /**
79
     * Method renders common parts of all pages.
80
     *
81
     * @return array List of common parts.
82
     */
83
    public function crossRender(): array
84
    {
85
        return [];
86
    }
87
88
    /**
89
     * Method formats exception object
90
     *
91
     * @param \Exception $e
92
     *            Exception
93
     * @return object Formatted exception object
94
     */
95
    protected function baseFormatter(\Exception $e): object
96
    {
97
        $error = new \stdClass();
98
        $error->message = $e->getMessage();
99
        $error->code = $e->getCode();
100
        $error->call_stack = $this->formatCallStack($e);
101
        if (isset($_SERVER['HTTP_HOST']) && $_SERVER['REQUEST_URI']) {
102
            $error->host = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
103
        } else {
104
            $error->host = 'undefined';
105
        }
106
        return $error;
107
    }
108
109
    /**
110
     * Method processes exception.
111
     *
112
     * @param Rest\Exception $e
113
     *            RestException object.
114
     */
115
    public function handleRestException(Rest\Exception $e): void
116
    {
117
        $error = $this->baseFormatter($e);
118
119
        $error->httpBody = $e->getHttpBody();
120
121
        print('<pre>' . json_encode($error, JSON_PRETTY_PRINT));
122
    }
123
124
    /**
125
     * Method processes exception.
126
     *
127
     * @param \Exception $e
128
     *            Exception object.
129
     */
130
    public function handleException(\Exception $e): void
131
    {
132
        $error = $this->baseFormatter($e);
133
134
        print('<pre>' . json_encode($error, JSON_PRETTY_PRINT));
135
    }
136
137
    /**
138
     * Method sets GET parameter as template var
139
     *
140
     * @param string $fieldName
141
     *            name of the GET parameter
142
     */
143
    protected function setGetVar(string $fieldName): void
144
    {
145
        if (isset($_GET[$fieldName])) {
146
            $this->template->setPageVar($fieldName, $_GET[$fieldName]);
147
        }
148
    }
149
150
    /**
151
     * Running application.
152
     */
153
    public function run(): void
154
    {
155
        try {
156
            $callRouteResult = $this->callRoute();
157
158
            if (is_array($callRouteResult) === false) {
159
                throw (new \Exception('Route was not called properly'));
160
            }
161
162
            $result = array_merge($callRouteResult, $this->crossRender());
0 ignored issues
show
Bug introduced by
It seems like $callRouteResult can also be of type null and string and true; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
            $result = array_merge(/** @scrutinizer ignore-type */ $callRouteResult, $this->crossRender());
Loading history...
163
164
            if (is_array($result)) {
0 ignored issues
show
introduced by
The condition is_array($result) is always true.
Loading history...
165
                foreach ($result as $key => $value) {
166
                    $content = $value instanceof ViewInterface ? $value->render() : $value;
167
168
                    $this->template->setPageVar($key, $content);
169
                }
170
            }
171
172
            $this->setGetVar('redirect-to');
173
174
            print($this->template->compile());
175
        } catch (Rest\Exception $e) {
176
            $this->handleRestException($e);
177
        } catch (\Exception $e) {
178
            $this->handleException($e);
179
        }
180
    }
181
182
    /**
183
     * Getting template
184
     *
185
     * @return HtmlTemplate Application's template
186
     * @codeCoverageIgnore
187
     */
188
    public function getTemplate(): HtmlTemplate
189
    {
190
        return $this->template;
191
    }
192
193
    /**
194
     * Setting template
195
     *
196
     * @param HtmlTemplate $template
197
     *            Template
198
     * @codeCoverageIgnore
199
     */
200
    public function setTemplate(HtmlTemplate $template): void
201
    {
202
        $this->template = $template;
203
    }
204
205
    /**
206
     * Method returns localized error message by it's key
207
     *
208
     * @param string $actionMessageCode
209
     *            key of the message
210
     * @return string localized error message by it's key
211
     */
212
    protected function getActionMessage(string $actionMessageCode): string
213
    {
214
        $classPath = $this->getClassPath();
215
216
        if (file_exists($classPath . '/res/action-messages.json')) {
217
            $messages = json_decode(file_get_contents($classPath . '/res/action-messages.json'), true);
218
219
            if (isset($messages[$actionMessageCode])) {
220
                return $messages[$actionMessageCode];
221
            } else {
222
                throw (new \Exception('The message with locator "' . $actionMessageCode . '" was not found', - 1));
223
            }
224
        }
225
226
        return '';
227
    }
228
229
    /**
230
     * Method returns action-message or '' if not set
231
     *
232
     * @return string
233
     */
234
    protected function getActionMessageCode(): string
235
    {
236
        return $_GET['action-message'] ?? '';
237
    }
238
239
    /**
240
     * Method sets message variable
241
     *
242
     * @param string $actionMessageCode
243
     *            message code
244
     */
245
    protected function setSuccessMessage(string $actionMessageCode): void
246
    {
247
        $this->getTemplate()->setPageVar('action-message', $this->getActionMessage($actionMessageCode));
248
    }
249
250
    /**
251
     * Method compiles result record
252
     *
253
     * @param mixed $presenter
254
     *            main area presenter
255
     * @return array result record
256
     */
257
    public function result($presenter = null): void
258
    {
259
        if ($presenter !== null) {
260
            $presenter->run();
261
        }
262
263
        if ($actionMessage = $this->getActionMessageCode()) {
264
            $this->setSuccessMessage($actionMessage);
265
        }
266
    }
267
268
    /**
269
     * Overriding defined config
270
     *
271
     * @param string $path
272
     *            path to the current config
273
     * @param array $config
274
     *            config itself
275
     */
276
    private function constructOverrideHandler(string $path, array &$config): void
277
    {
278
        if (isset($config['override'])) {
279
280
            $path = pathinfo($path, PATHINFO_DIRNAME);
281
282
            $baseConfig = json_decode(file_get_contents($path . '/' . $config['override']), true);
283
284
            $config = array_merge($baseConfig, $config);
285
        }
286
    }
287
288
    /**
289
     * Constructing view
290
     *
291
     * @param array $result
292
     *            compiled result
293
     * @param string $key
294
     *            config key
295
     * @param mixed $value
296
     *            config value
297
     * @param array $views
298
     *            list of views
299
     */
300
    private function constructOtherView(array &$result, string $key, $value, array &$views): void
301
    {
302
        // any other view
303
        if (isset($value['name'])) {
304
            $views[$key] = new $value['class']($this->getTemplate(), $value['name']);
305
        } else {
306
            $views[$key] = new $value['class']($this->getTemplate());
307
        }
308
        $result[$value['placeholder']] = $views[$key];
309
    }
310
311
    /**
312
     * Method returns fabric method for action processing
313
     *
314
     * @param string $key
315
     *            config key name
316
     * @param mixed $value
317
     *            config key value
318
     * @return callable|NULL callback
319
     */
320
    private function getActionBuilderMethod(string $key, $value): ?callable
321
    {
322
        if ($key === 'override') {
323
            return ActionBuilder::ignoreKey();
324
        } elseif ($key === 'layout') {
325
            return ActionBuilder::resetLayout($this->getTemplate(), $value);
326
        }
327
328
        return null;
329
    }
330
331
    /**
332
     * Method creates action from JSON config
333
     *
334
     * @param string $path
335
     *            path to JSON config
336
     */
337
    private function createActionFromJsonConfig(string $path): void
338
    {
339
        $method = 'action' . basename($path, '.json');
340
341
        $this->$method = function () use ($path): array {
342
            $result = [];
343
            $views = [];
344
            $presenter = null;
345
            $config = json_decode(file_get_contents($path), true);
346
347
            $this->constructOverrideHandler($path, $config);
348
349
            foreach ($config as $key => $value) {
350
                $callback = $this->getActionBuilderMethod($key, $value);
351
352
                if ($callback !== null) {
353
                    $callback();
354
                } elseif (is_string($value)) {
355
                    // string content
356
                    $result[$key] = $value;
357
                } elseif ($key === 'presenter') {
358
                    $presenter = new $value['class'](
359
                        isset($value['view']) && isset($views[$value['view']]) ? $views[$value['view']] : null,
360
                        $value['name'],
361
                        $this->getRequestParamsFetcher());
362
                } else {
363
                    $this->constructOtherView($result, $key, $value, $views);
364
                }
365
            }
366
367
            $this->result($presenter);
368
369
            return $result;
370
        };
371
372
        $this->loadRoute(
373
            [
374
                'route' => Utils::convertMethodNameToRoute($method),
375
                'callback' => [
376
                    $this,
377
                    $method
378
                ],
379
                'method' => [
380
                    'GET',
381
                    'POST'
382
                ]
383
            ]);
384
    }
385
386
    /**
387
     * Method loads actions from path
388
     *
389
     * @param string $path
390
     */
391
    protected function loadActionsFromDirectory(string $path): void
392
    {
393
        if (file_exists($path)) {
394
            $files = scandir($path);
395
396
            foreach ($files as $file) {
397
                if (is_file($path . '/' . $file) && strpos($file, '.json') !== false) {
398
                    $this->createActionFromJsonConfig($path . '/' . $file);
399
                }
400
            }
401
        }
402
    }
403
404
    /**
405
     * Method loads all actions from ./actions directory
406
     */
407
    private function loadActoinsFromConfig(): void
408
    {
409
        // TODO read actions from /Actions/
410
        $this->loadActionsFromDirectory($this->getClassPath() . '/actions/');
411
    }
412
}
413
414