Completed
Branch master (6f5620)
by Anton
02:08
created

Controller   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 68.1%

Importance

Changes 0
Metric Value
dl 0
loc 347
ccs 79
cts 116
cp 0.681
rs 8.2857
c 0
b 0
f 0
wmc 39
lcom 1
cbo 14

13 Methods

Rating   Name   Duplication   Size   Complexity  
A setFile() 0 11 2
C checkAccept() 0 42 8
A checkMethod() 0 7 3
A __construct() 0 9 1
A checkPrivilege() 0 7 3
C run() 0 61 9
A getFile() 0 7 2
A setMeta() 0 18 2
A getMeta() 0 7 2
A assign() 0 5 1
A getData() 0 7 2
A jsonSerialize() 0 4 1
B __toString() 0 33 3
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
declare(strict_types=1);
10
11
namespace Bluz\Controller;
12
13
use Bluz\Application\Application;
14
use Bluz\Application\Exception\ForbiddenException;
15
use Bluz\Application\Exception\NotAcceptableException;
16
use Bluz\Application\Exception\NotAllowedException;
17
use Bluz\Auth\EntityInterface;
18
use Bluz\Common\Helper;
19
use Bluz\Proxy\Acl;
20
use Bluz\Proxy\Cache;
21
use Bluz\Proxy\Logger;
22
use Bluz\Proxy\Request;
23
use Bluz\Response\ResponseTrait;
24
use Bluz\View\View;
25
26
/**
27
 * Statement
28
 *
29
 * @package  Bluz\Controller
30
 * @author   Anton Shevchuk
31
 *
32
 * @method void attachment(string $file)
33
 * @method void denied()
34
 * @method void disableLayout()
35
 * @method void disableView()
36
 * @method Controller dispatch(string $module, string $controller, array $params = [])
37
 * @method void redirect(string $url)
38
 * @method void redirectTo(string $module, string $controller, array $params = [])
39
 * @method void reload()
40
 * @method void useJson()
41
 * @method void useLayout($layout)
42
 * @method EntityInterface user()
43
 */
44
class Controller implements \JsonSerializable
45
{
46
    use Helper;
47
    use ResponseTrait;
48
49
    /**
50
     * @var string
51
     */
52
    protected $module;
53
54
    /**
55
     * @var string
56
     */
57
    protected $controller;
58
59
    /**
60
     * @var string
61
     */
62
    protected $template;
63
64
    /**
65
     * @var string
66
     */
67
    protected $file;
68
69
    /**
70
     * @var Meta
71
     */
72
    protected $meta;
73
74
    /**
75
     * @var Data
76
     */
77
    protected $data;
78
79
    /**
80
     * One of HTML, JSON or empty string
81
     *
82
     * @var string
83
     */
84
    protected $render = 'HTML';
85
86
    /**
87
     * Constructor of Statement
88
     *
89
     * @param string $module
90
     * @param string $controller
91
     */
92 715
    public function __construct($module, $controller)
93
    {
94 715
        $this->module = $module;
95 715
        $this->controller = $controller;
96 715
        $this->template = $controller . '.phtml';
97
98
        // initial default helper path
99 715
        $this->addHelperPath(__DIR__ . '/Helper/');
100 715
    }
101
102
    /**
103
     * Check `Privilege`
104
     *
105
     * @throws ForbiddenException
106
     */
107 14
    public function checkPrivilege()
108
    {
109 14
        $privilege = $this->getMeta()->getPrivilege();
110 13
        if ($privilege && !Acl::isAllowed($this->module, $privilege)) {
111
            throw new ForbiddenException;
112
        }
113 13
    }
114
115
    /**
116
     * Check `Method`
117
     *
118
     * @throws NotAllowedException
119
     */
120 14
    public function checkMethod()
121
    {
122 14
        $methods = $this->getMeta()->getMethod();
123 13
        if ($methods && !in_array(Request::getMethod(), $methods)) {
124
            throw new NotAllowedException(implode(',', $methods));
125
        }
126 13
    }
127
128
    /**
129
     * Check `Accept`
130
     *
131
     * @throws NotAcceptableException
132
     */
133 14
    public function checkAccept()
134
    {
135
        // all ok for CLI
136 14
        if (PHP_SAPI === 'cli') {
137 14
            return;
138
        }
139
140
        $allowAccept = $this->getMeta()->getAccept();
141
142
        // some controllers hasn't @accept tag
143
        if (!$allowAccept) {
144
            // but by default allow just HTML output
145
            $allowAccept = [Request::TYPE_HTML, Request::TYPE_ANY];
146
        }
147
148
        // get Accept with high priority
149
        $accept = Request::checkAccept($allowAccept);
150
151
        // some controllers allow any type (*/*)
152
        // and client doesn't send Accept header
153
        if (in_array(Request::TYPE_ANY, $allowAccept) && !$accept) {
154
            // all OK, controller should realize logic for response
155
            return;
156
        }
157
158
        // some controllers allow just selected types
159
        // choose MIME type by browser accept header
160
        // filtered by controller @accept
161
        // switch statement for this logic
162
        switch ($accept) {
163
            case Request::TYPE_ANY:
164
            case Request::TYPE_HTML:
165
                // HTML response with layout
166
                break;
167
            case Request::TYPE_JSON:
168
                // JSON response
169
                $this->template = null;
170
                break;
171
            default:
172
                throw new NotAcceptableException;
173
        }
174
    }
175
176
    /**
177
     * __invoke
178
     *
179
     * @param array $params
180
     *
181
     * @return Data
182
     * @throws ControllerException
183
     */
184 13
    public function run(array $params = [])
185
    {
186
        // initial variables for use inside controller
187 13
        $module = $this->module;
188 13
        $controller = $this->controller;
189
190 13
        $cacheKey = "data.$module.$controller." . md5(http_build_query($params));
191
192 13
        $cacheTime = $this->getMeta()->getCache();
193
194 13
        if ($cacheTime && $cached = Cache::get($cacheKey)) {
195
            $this->data = $cached;
196
            return $cached;
197
        }
198
199
        /**
200
         * @var \closure $controllerClosure
201
         */
202 13
        $controllerClosure = include $this->getFile();
203
204 13
        if (!is_callable($controllerClosure)) {
205
            throw new ControllerException("Controller is not callable '{$module}/{$controller}'");
206
        }
207
208
        // process params
209 13
        $params = $this->getMeta()->params($params);
210
211
        // call Closure or Controller
212 13
        $result = $controllerClosure(...$params);
213
214
        // switch statement for result of Closure run
215
        switch (true) {
216 11
            case ($result === false):
217
                // return "false" is equal to disable view and layout
218 5
                $this->disableLayout();
219 5
                $this->disableView();
220 5
                break;
221 6
            case is_string($result):
222
                // return string variable is equal to change view template
223
                $this->template = $result;
224
                break;
225 6
            case is_array($result):
226
                // return associative array is equal to setup view data
227 3
                $this->getData()->setFromArray($result);
228 3
                break;
229 3
            case ($result instanceof Controller):
230
                $this->getData()->setFromArray($result->getData()->toArray());
231
                break;
232
        }
233
234 11
        if ($cacheTime) {
235
            Cache::set(
236
                $cacheKey,
237
                $this->getData(),
238
                $cacheTime,
239
                ['system', 'data', Cache::prepare("$module.$controller")]
240
            );
241
        }
242
243 11
        return $this->getData();
244
    }
245
246
    /**
247
     * Setup controller file
248
     *
249
     * @return void
250
     * @throws ControllerException
251
     */
252 715
    protected function setFile()
253
    {
254 715
        $path = Application::getInstance()->getPath();
255 715
        $file = "$path/modules/{$this->module}/controllers/{$this->controller}.php";
256
257 715
        if (!file_exists($file)) {
258 3
            throw new ControllerException("Controller file not found '{$this->module}/{$this->controller}'", 404);
259
        }
260
261 715
        $this->file = $file;
262 715
    }
263
264
    /**
265
     * Get controller file path
266
     *
267
     * @return string
268
     */
269 715
    protected function getFile() // : string
270
    {
271 715
        if (!$this->file) {
272 715
            $this->setFile();
273
        }
274 715
        return $this->file;
275
    }
276
277
    /**
278
     * Retrieve reflection for anonymous function
279
     *
280
     * @return void
281
     * @throws \Bluz\Common\Exception\ComponentException
282
     */
283 715
    protected function setMeta()
284
    {
285
        // cache for reflection data
286 715
        $cacheKey = "meta.{$this->module}.{$this->controller}";
287
288 715
        if (!$meta = Cache::get($cacheKey)) {
289 715
            $meta = new Meta($this->getFile());
290 715
            $meta->process();
291
292 715
            Cache::set(
293 715
                $cacheKey,
294 715
                $meta,
295 715
                Cache::TTL_NO_EXPIRY,
296 715
                ['system', 'meta', Cache::prepare($this->module . '.' . $this->controller)]
297
            );
298
        }
299 715
        $this->meta = $meta;
300 715
    }
301
302
    /**
303
     * Get meta information
304
     *
305
     * @return Meta
306
     */
307 715
    public function getMeta()
308
    {
309 715
        if (!$this->meta) {
310 715
            $this->setMeta();
311
        }
312 715
        return $this->meta;
313
    }
314
315
    /**
316
     * Assign key/value pair to Data object
317
     *
318
     * @param  string $key
319
     * @param  mixed  $value
320
     *
321
     * @return Controller
322
     */
323
    public function assign($key, $value)
324
    {
325
        $this->getData()->set($key, $value);
326
        return $this;
327
    }
328
329
    /**
330
     * Get controller Data container
331
     *
332
     * @return Data
333
     */
334 11
    public function getData()
335
    {
336 11
        if (!$this->data) {
337 11
            $this->data = new Data();
338
        }
339 11
        return $this->data;
340
    }
341
342
    /**
343
     * Specify data which should be serialized to JSON
344
     *
345
     * @return Data
346
     */
347
    public function jsonSerialize()
348
    {
349
        return $this->getData();
350
    }
351
352
    /**
353
     * Magic cast to string
354
     *
355
     * @return string
356
     */
357 2
    public function __toString()
358
    {
359 2
        if (!$this->template) {
360
            return '';
361
        }
362
363
        try {
364
            // $view for use in closure
365 2
            $view = new View();
366
367 2
            $path = Application::getInstance()->getPath();
368
369
            // setup additional helper path
370 2
            $view->addHelperPath($path . '/layouts/helpers');
371
372
            // setup additional partial path
373 2
            $view->addPartialPath($path . '/layouts/partial');
374
375
            // setup default path
376 2
            $view->setPath($path . '/modules/' . $this->module . '/views');
377
378
            // setup template
379 2
            $view->setTemplate($this->template);
380
381
            // setup data
382 2
            $view->setFromArray($this->getData()->toArray());
383 2
            return $view->render();
384
        } catch (\Exception $e) {
385
            // save log
386
            Logger::error($e->getMessage());
387
            return '';
388
        }
389
    }
390
}
391