Completed
Push — feature/controller ( caab25...3c87ac )
by René
02:41
created

Controller::setResponseCode()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 54
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 10.8889

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 54
ccs 20
cts 28
cp 0.7143
rs 7.255
cc 9
eloc 29
nc 9
nop 1
crap 10.8889

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
declare(strict_types = 1);
3
4
namespace Zortje\MVC\Controller;
5
6
use Zortje\MVC\Configuration\Configuration;
7
use Zortje\MVC\Controller\Exception\ControllerActionNonexistentException;
8
use Zortje\MVC\Controller\Exception\ControllerActionPrivateInsufficientAuthenticationException;
9
use Zortje\MVC\Controller\Exception\ControllerActionProtectedInsufficientAuthenticationException;
10
use Zortje\MVC\Model\Table\Entity\Entity;
11
use Zortje\MVC\Network\Request;
12
use Zortje\MVC\Network\Response;
13
use Zortje\MVC\View\Render\HtmlRender;
14
15
/**
16
 * Class Controller
17
 *
18
 * @package Zortje\MVC\Controller
19
 */
20
class Controller
21
{
22
23
    /**
24
     * Controller action is publicly accessible
25
     */
26
    const ACTION_PUBLIC = 0;
27
28
    /**
29
     * Controller action requires authentication
30
     * Will redirect to sign in page if not authenticated
31
     */
32
    const ACTION_PROTECTED = 1;
33
34
    /**
35
     * Controller action requires authentication
36
     * Will result in an 404 if not authenticated
37
     */
38
    const ACTION_PRIVATE = 2;
39
40
    /**
41
     * @var array Controller action access rules
42
     */
43
    protected $access = [];
44
45
    /**
46
     * @var \PDO PDO
47
     */
48
    protected $pdo;
49
50
    /**
51
     * @var Configuration
52
     */
53
    protected $configuration;
54
55
    /**
56
     * @var Request
57
     */
58
    protected $request;
59
60
    /**
61
     * @var Entity|null User
62
     */
63
    protected $user;
64
65
    /**
66
     * @var string Controller action
67
     */
68
    protected $action;
69
70
    /**
71
     * @var array View variables
72
     */
73
    protected $variables = [];
74
75
    /**
76
     * @var bool Should render view for controller action
77
     */
78
    protected $render = true;
79
80
    /**
81
     * @var string File path for layout template file
82
     */
83
    protected $layout;
84
85
    /**
86
     * @var string File path for view template file
87
     */
88
    protected $view;
89
90
    /**
91
     * @var string Content type
92
     */
93
    protected $contentType = 'html';
94
95
    /**
96
     * @var array Headers for output
97
     */
98
    protected $headers = [];
99
100
    /**
101
     * Controller constructor.
102
     *
103
     * @param \PDO          $pdo
104
     * @param Configuration $configuration
105
     * @param Request       $request
106
     * @param Entity|null   $user
107
     */
108 1 View Code Duplication
    public function __construct(\PDO $pdo, Configuration $configuration, Request $request, Entity $user = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
109
    {
110 1
        $this->pdo           = $pdo;
111 1
        $this->configuration = $configuration;
112 1
        $this->request       = $request;
113 1
        $this->user          = $user;
114 1
    }
115
116
    /**
117
     * @return string Controller name without namespace
118
     */
119 1
    public function getShortName(): string
120
    {
121 1
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
122
    }
123
124
    /**
125
     * @param string $action Controller action
126
     *
127
     * @throws ControllerActionNonexistentException
128
     * @throws ControllerActionPrivateInsufficientAuthenticationException
129
     * @throws ControllerActionProtectedInsufficientAuthenticationException
130
     */
131 4
    public function setAction(string $action)
132
    {
133
        /**
134
         * Check if method exists and that access has been defined
135
         */
136 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
137 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
138
        }
139
140
        /**
141
         * Check controller action access level if user is not authenticated
142
         */
143 3
        if (!$this->user) {
144 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
145 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
146 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
147 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
148
            }
149
        }
150
151
        /**
152
         * Set controller action
153
         */
154 1
        $this->action = $action;
155 1
    }
156
157
    /**
158
     * Call action
159
     *
160
     * @return Response
161
     *
162
     * @throws \LogicException If controller action is not set
163
     */
164
    public function callAction(): Response
165
    {
166
        if (!isset($this->action)) {
167
            throw new \LogicException('Controller action must be set before being called');
168
        }
169
170
        /**
171
         * Before controller action hook
172
         */
173
        if ($this->beforeAction()) {
174
            /**
175
             * Call controller action
176
             */
177
            $action = $this->action;
178
179
            $this->$action();
0 ignored issues
show
Security Code Execution introduced by
$action can contain request data and is used in code execution context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
180
181
            /**
182
             * After controller action hook
183
             */
184
            $this->afterAction();
185
        }
186
187
        /**
188
         * Render view
189
         */
190
        if ($this->render && $this->contentType === 'html') {
191
            if ($this->request->getCookie()->exists('Flash.Message') && $this->request->getCookie()->exists('Flash.Type')) {
192
                $this->set('_flash', [
193
                    'message' => $this->request->getCookie()->get('Flash.Message'),
194
                    'type'    => $this->request->getCookie()->get('Flash.Type')
195
                ]);
196
197
                $this->request->getCookie()->remove('Flash.Message');
198
                $this->request->getCookie()->remove('Flash.Type');
199
            }
200
201
            /**
202
             * Set content type header
203
             */
204
            $this->headers['content-type'] = 'Content-Type: text/html; charset=utf-8';
205
206
            /**
207
             * Render output
208
             */
209
            $render = new HtmlRender($this->variables);
210
211
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
212
213
            /**
214
             * Return response
215
             */
216
            return new Response($this->headers, $this->request->getCookie(), $output);
217
        } elseif ($this->render && $this->contentType === 'json') {
218
            /**
219
             * Set content type header
220
             */
221
            $this->headers['content-type'] = 'Content-Type: application/javascript;';
222
223
            /**
224
             * Return response
225
             */
226
            return new Response($this->headers, null, json_encode($this->variables));
227
        }
228
229
        return new Response($this->headers, null, '');
230
    }
231
232
    /**
233
     * Before controller action hook
234
     *
235
     * Called right before controller action is called
236
     *
237
     * If FALSE is returned, the action will not be called
238
     *
239
     * @return bool
240
     */
241
    protected function beforeAction(): bool
242
    {
243
        return true;
244
    }
245
246
    /**
247
     * After controller action hook
248
     *
249
     * Called right after controller action is called, before rendering of the view
250
     *
251
     * Only called if action is called
252
     */
253
    protected function afterAction()
254
    {
255
    }
256
257
    /**
258
     * Set view variable
259
     *
260
     * @param string $variable
261
     * @param mixed  $value
262
     */
263
    protected function set(string $variable, $value)
264
    {
265
        $this->variables[$variable] = $value;
266
    }
267
268
    /**
269
     * Set flash message
270
     *
271
     * Recommended types: error, warning, success & info
272
     *
273
     * @param string $message Flash message
274
     * @param string $type    Flash type
275
     */
276
    protected function setFlash(string $message, string $type)
277
    {
278
        $cookie = $this->request->getCookie();
279
280
        $cookie->set('Flash.Message', $message);
281
        $cookie->set('Flash.Type', $type);
282
    }
283
284
    /**
285
     * Set a redirect header in the response
286
     *
287
     * @param string $url URL for redirect
288
     */
289
    protected function redirect(string $url)
290
    {
291
        $this->headers['locaction'] = "Location: $url";
292
293
        /**
294
         * Disable rendering if redirecting
295
         */
296
        $this->render = false;
297
    }
298
299
    /**
300
     * Get layout template
301
     *
302
     * @return string Layout template file path
303
     */
304 1
    protected function getLayoutTemplate(): string
305
    {
306 1
        $layout = $this->layout;
307
308 1
        if (empty($layout)) {
309
            $layout = 'View/Layout/default';
310
        }
311
312 1
        return "{$this->configuration->get('App.Path')}$layout.layout";
313
    }
314
315
    /**
316
     * Get view template
317
     *
318
     * @return string View template file path
319
     */
320 2
    protected function getViewTemplate(): string
321
    {
322 2
        $view = $this->view;
323
324 2
        if (empty($view)) {
325 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
326
        }
327
328 2
        return "{$this->configuration->get('App.Path')}$view.view";
329
    }
330
331
    /**
332
     * Set response code
333
     *
334
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
335
     *
336
     * @param int $code HTTP response code
337
     *
338
     * @throws \InvalidArgumentException If unsupported code is provided
339
     */
340 5
    protected function setResponseCode(int $code)
341
    {
342
        switch ($code) {
343
            /**
344
             * Success
345
             */
346 5
            case 200:
347 1
                $text = 'OK';
348 1
                break;
349
350 4
            case 201:
351
                $text = 'Created';
352
                break;
353
354 4
            case 202:
355
                $text = 'Accepted';
356
                break;
357
358 4
            case 204:
359
                $text = 'No Content';
360
                break;
361
362
            /**
363
             * Client error
364
             */
365 4
            case 400:
366
                $text = 'Bad Request';
367
                break;
368
369 4
            case 403:
370 1
                $text = 'Forbidden';
371 1
                break;
372
373 3
            case 404:
374 1
                $text = 'Not Found';
375 1
                break;
376
377
            /**
378
             * Server error
379
             */
380 2
            case 500:
381 1
                $text = 'Internal Server Error';
382 1
                break;
383
384
            default:
385 1
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
386
        }
387
388
        /**
389
         * Set header
390
         */
391
        // @todo test that running response code multiple times only results in one response code header
392 4
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
393 4
    }
394
}
395