Completed
Push — feature/controller ( 4da62c...5a6415 )
by René
04:10
created

Controller::afterAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace Zortje\MVC\Controller;
5
6
use Zortje\MVC\Controller\Exception\ControllerActionNonexistentException;
7
use Zortje\MVC\Controller\Exception\ControllerActionPrivateInsufficientAuthenticationException;
8
use Zortje\MVC\Controller\Exception\ControllerActionProtectedInsufficientAuthenticationException;
9
use Zortje\MVC\Storage\Cookie\Cookie;
10
use Zortje\MVC\User\User;
11
use Zortje\MVC\View\Render\HtmlRender;
12
13
/**
14
 * Class Controller
15
 *
16
 * @package Zortje\MVC\Controller
17
 */
18
class Controller
19
{
20
21
    /**
22
     * Controller action is publicly accessible
23
     */
24
    const ACTION_PUBLIC = 0;
25
26
    /**
27
     * Controller action requires authentication
28
     * Will redirect to sign in page if not authenticated
29
     */
30
    const ACTION_PROTECTED = 1;
31
32
    /**
33
     * Controller action requires authentication
34
     * Will result in an 404 if not authenticated
35
     */
36
    const ACTION_PRIVATE = 2;
37
38
    /**
39
     * @var array Controller action access rules
40
     */
41
    protected $access = [];
42
43
    /**
44
     * @var \PDO PDO
45
     */
46
    protected $pdo;
47
48
    /**
49
     * @var array Post
50
     */
51
    protected $post;
52
53
    /**
54
     * @var Cookie Cookie
55
     */
56
    protected $cookie;
57
58
    /**
59
     * @var User|null User
60
     */
61
    protected $user;
62
63
    /**
64
     * @var string App file path
65
     */
66
    protected $appPath;
67
68
    /**
69
     * @var string Controller action
70
     */
71
    protected $action;
72
73
    /**
74
     * @var array View variables
75
     */
76
    protected $variables = [];
77
78
    /**
79
     * @var bool Should render view for controller action
80
     */
81
    protected $render = true;
82
83
    /**
84
     * @var string File path for layout template file
85
     */
86
    protected $layout;
87
88
    /**
89
     * @var string File path for view template file
90
     */
91
    protected $view;
92
93
    /**
94
     * @var array Headers for output
95
     *
96
     * @todo JSON content type: `Content-Type: application/javascript; charset=utf-8`
97
     */
98
    protected $headers = [
99
        'content-type' => 'Content-Type: text/html; charset=utf-8'
100
    ];
101
102
    /**
103
     * Controller constructor.
104
     *
105
     * @param \PDO      $pdo
106
     * @param array     $post
107
     * @param Cookie    $cookie
108
     * @param User|null $user
109
     * @param string    $appPath
110
     */
111 1 View Code Duplication
    public function __construct(\PDO $pdo, array $post, Cookie $cookie, string $appPath, User $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...
112
    {
113 1
        $this->pdo     = $pdo;
114 1
        $this->post    = $post;
115 1
        $this->cookie  = $cookie;
116 1
        $this->user    = $user;
117 1
        $this->appPath = $appPath;
118 1
    }
119
120
    /**
121
     * @return string Controller name without namespace
122
     */
123 1
    public function getShortName(): string
124
    {
125 1
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
126
    }
127
128
    /**
129
     * @param string $action Controller action
130
     *
131
     * @throws ControllerActionNonexistentException
132
     * @throws ControllerActionPrivateInsufficientAuthenticationException
133
     * @throws ControllerActionProtectedInsufficientAuthenticationException
134
     */
135 4
    public function setAction(string $action)
136
    {
137
        /**
138
         * Check if method exists and that access has been defined
139
         */
140 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
141 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
142
        }
143
144
        /**
145
         * Check controller action access level if user is not authenticated
146
         */
147 3
        if (!$this->user) {
148 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
149 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
150 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
151 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
152
            }
153
        }
154
155
        /**
156
         * Set controller action
157
         */
158 1
        $this->action = $action;
159 1
    }
160
161
    /**
162
     * Call action
163
     *
164
     * @return array<string,array|string> Headers and output if render is enabled, otherwise FALSE
165
     *
166
     * @throws \LogicException If controller action is not set
167
     */
168
    public function callAction(): array
169
    {
170
        if (!isset($this->action)) {
171
            throw new \LogicException('Controller action must be set before being called');
172
        }
173
174
        /**
175
         * Before controller action hook
176
         */
177
        $this->beforeAction();
178
179
        /**
180
         * Call controller action
181
         */
182
        $action = $this->action;
183
184
        $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...
185
186
        /**
187
         * After controller action hook
188
         */
189
        $this->afterAction();
190
191
        /**
192
         * Render view
193
         */
194
        if ($this->render) {
195
            $render = new HtmlRender($this->variables);
196
197
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
198
199
            return [
200
                'headers'      => $this->headers,
201
                'cookie_token' => $this->cookie,
202
                'output'       => $output
203
            ];
204
        }
205
206
        return [];
207
    }
208
209
    /**
210
     * Before controller action hook
211
     *
212
     * Called right before controller action is called
213
     */
214
    protected function beforeAction()
215
    {
216
    }
217
218
    /**
219
     * After controller action hook
220
     *
221
     * Called right after controller action is called, but before rendering of the view
222
     */
223
    protected function afterAction()
224
    {
225
    }
226
227
    /**
228
     * Set view variable
229
     *
230
     * @param string $variable
231
     * @param mixed  $value
232
     */
233
    protected function set(string $variable, $value)
234
    {
235
        $this->variables[$variable] = $value;
236
    }
237
238
    /**
239
     * Get layout template
240
     *
241
     * @return string Layout template file path
242
     */
243 1
    protected function getLayoutTemplate(): string
244
    {
245 1
        $layout = $this->layout;
246
247 1
        if (empty($layout)) {
248
            $layout = 'View/Layout/default';
249
        }
250
251 1
        return "{$this->appPath}$layout.layout";
252
    }
253
254
    /**
255
     * Get view template
256
     *
257
     * @return string View template file path
258
     */
259 2
    protected function getViewTemplate(): string
260
    {
261 2
        $view = $this->view;
262
263 2
        if (empty($view)) {
264 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
265
        }
266
267 2
        return "{$this->appPath}$view.view";
268
    }
269
270
    /**
271
     * Set response code
272
     *
273
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
274
     *
275
     * @param int $code HTTP response code
276
     *
277
     * @throws \InvalidArgumentException If unsupported code is provided
278
     */
279 5
    protected function setResponseCode(int $code)
280
    {
281
        switch ($code) {
282 5
            case 200:
283 1
                $text = 'OK';
284 1
                break;
285
286
            case 403:
287 1
                $text = 'Forbidden';
288 1
                break;
289
290
            case 404:
291 1
                $text = 'Not Found';
292 1
                break;
293
294 1
            case 500:
295 1
                $text = 'Internal Server Error';
296 1
                break;
297
298
            default:
299 1
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
300
        }
301
302
        /**
303
         * Set header
304
         */
305
        // @todo test that running response code multiple times only results in one response code header
306 4
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
307 4
    }
308
}
309