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 array URL Arguments |
67
|
|
|
*/ |
68
|
|
|
protected $arguments; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var string Controller action |
72
|
|
|
*/ |
73
|
|
|
protected $action; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @var array View variables |
77
|
|
|
*/ |
78
|
|
|
protected $variables = []; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @var bool Should render view for controller action |
82
|
|
|
*/ |
83
|
|
|
protected $render = true; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @var string File path for layout template file |
87
|
|
|
*/ |
88
|
|
|
protected $layout; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* @var string File path for view template file |
92
|
|
|
*/ |
93
|
|
|
protected $view; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @var string Content type |
97
|
|
|
*/ |
98
|
|
|
protected $contentType = 'html'; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @var array Headers for output |
102
|
|
|
*/ |
103
|
|
|
protected $headers = []; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Controller constructor. |
107
|
|
|
* |
108
|
|
|
* @param \PDO $pdo |
109
|
|
|
* @param Configuration $configuration |
110
|
|
|
* @param Request $request |
111
|
|
|
* @param Entity|null $user |
112
|
|
|
*/ |
113
|
1 |
View Code Duplication |
public function __construct(\PDO $pdo, Configuration $configuration, Request $request, Entity $user = null) |
|
|
|
|
114
|
|
|
{ |
115
|
1 |
|
$this->pdo = $pdo; |
116
|
1 |
|
$this->configuration = $configuration; |
117
|
1 |
|
$this->request = $request; |
118
|
1 |
|
$this->user = $user; |
119
|
1 |
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Get short controller name |
123
|
|
|
* |
124
|
|
|
* @return string Controller name without namespace |
125
|
|
|
*/ |
126
|
1 |
|
public function getShortName(): string |
127
|
|
|
{ |
128
|
1 |
|
return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName()); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Set URL arguments |
133
|
|
|
* |
134
|
|
|
* @param array $arguments |
135
|
|
|
*/ |
136
|
1 |
|
public function setArguments(array $arguments) |
137
|
|
|
{ |
138
|
1 |
|
$this->arguments = $arguments; |
139
|
1 |
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Set controller action |
143
|
|
|
* |
144
|
|
|
* @param string $action Controller action |
145
|
|
|
* |
146
|
|
|
* @throws ControllerActionNonexistentException |
147
|
|
|
* @throws ControllerActionPrivateInsufficientAuthenticationException |
148
|
|
|
* @throws ControllerActionProtectedInsufficientAuthenticationException |
149
|
|
|
*/ |
150
|
4 |
|
public function setAction(string $action) |
151
|
|
|
{ |
152
|
|
|
/** |
153
|
|
|
* Check if method exists and that access has been defined |
154
|
|
|
*/ |
155
|
4 |
|
if (!method_exists($this, $action) || !isset($this->access[$action])) { |
156
|
1 |
|
throw new ControllerActionNonexistentException([get_class($this), $action]); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Check controller action access level if user is not authenticated |
161
|
|
|
*/ |
162
|
3 |
|
if (!$this->user) { |
163
|
3 |
|
if ($this->access[$action] === self::ACTION_PRIVATE) { |
164
|
1 |
|
throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]); |
165
|
2 |
|
} elseif ($this->access[$action] === self::ACTION_PROTECTED) { |
166
|
1 |
|
throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]); |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Set controller action |
172
|
|
|
*/ |
173
|
1 |
|
$this->action = $action; |
174
|
1 |
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Call action |
178
|
|
|
* |
179
|
|
|
* @return Response |
180
|
|
|
* |
181
|
|
|
* @throws \LogicException If controller action is not set |
182
|
|
|
*/ |
183
|
|
|
public function callAction(): Response |
184
|
|
|
{ |
185
|
|
|
if (!isset($this->action)) { |
186
|
|
|
throw new \LogicException('Controller action must be set before being called'); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Before controller action hook |
191
|
|
|
*/ |
192
|
|
|
if ($this->beforeAction()) { |
193
|
|
|
/** |
194
|
|
|
* Call controller action |
195
|
|
|
*/ |
196
|
|
|
$action = $this->action; |
197
|
|
|
|
198
|
|
|
$this->$action(); |
|
|
|
|
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* After controller action hook |
202
|
|
|
*/ |
203
|
|
|
$this->afterAction(); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Render view |
208
|
|
|
*/ |
209
|
|
|
if ($this->render && $this->contentType === 'html') { |
210
|
|
|
if ($this->request->getCookie()->exists('Flash.Message') && $this->request->getCookie()->exists('Flash.Type')) { |
211
|
|
|
$this->set('_flash', [ |
212
|
|
|
'message' => $this->request->getCookie()->get('Flash.Message'), |
213
|
|
|
'type' => $this->request->getCookie()->get('Flash.Type') |
214
|
|
|
]); |
215
|
|
|
|
216
|
|
|
$this->request->getCookie()->remove('Flash.Message'); |
217
|
|
|
$this->request->getCookie()->remove('Flash.Type'); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Set content type header |
222
|
|
|
*/ |
223
|
|
|
$this->headers['content-type'] = 'Content-Type: text/html; charset=utf-8'; |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Render output |
227
|
|
|
*/ |
228
|
|
|
$render = new HtmlRender($this->variables); |
229
|
|
|
|
230
|
|
|
$output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]); |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Return response |
234
|
|
|
*/ |
235
|
|
|
return new Response($this->headers, $this->request->getCookie(), $output); |
236
|
|
|
} elseif ($this->render && $this->contentType === 'json') { |
237
|
|
|
/** |
238
|
|
|
* Set content type header |
239
|
|
|
*/ |
240
|
|
|
$this->headers['content-type'] = 'Content-Type: application/javascript;'; |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Return response |
244
|
|
|
*/ |
245
|
|
|
return new Response($this->headers, null, json_encode($this->variables)); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
return new Response($this->headers, null, ''); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Before controller action hook |
253
|
|
|
* |
254
|
|
|
* Called right before controller action is called |
255
|
|
|
* |
256
|
|
|
* If FALSE is returned, the action will not be called |
257
|
|
|
* |
258
|
|
|
* @return bool |
259
|
|
|
*/ |
260
|
|
|
protected function beforeAction(): bool |
261
|
|
|
{ |
262
|
|
|
return true; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* After controller action hook |
267
|
|
|
* |
268
|
|
|
* Called right after controller action is called, before rendering of the view |
269
|
|
|
* |
270
|
|
|
* Only called if action is called |
271
|
|
|
*/ |
272
|
|
|
protected function afterAction() |
273
|
|
|
{ |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Set view variable |
278
|
|
|
* |
279
|
|
|
* @param string $variable |
280
|
|
|
* @param mixed $value |
281
|
|
|
*/ |
282
|
1 |
|
protected function set(string $variable, $value) |
283
|
|
|
{ |
284
|
1 |
|
$this->variables[$variable] = $value; |
285
|
1 |
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Set flash message |
289
|
|
|
* |
290
|
|
|
* Recommended types: error, warning, success & info |
291
|
|
|
* |
292
|
|
|
* @param string $message Flash message |
293
|
|
|
* @param string $type Flash type |
294
|
|
|
*/ |
295
|
|
|
protected function setFlash(string $message, string $type) |
296
|
|
|
{ |
297
|
|
|
$cookie = $this->request->getCookie(); |
298
|
|
|
|
299
|
|
|
$cookie->set('Flash.Message', $message); |
300
|
|
|
$cookie->set('Flash.Type', $type); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Set a redirect header in the response |
305
|
|
|
* |
306
|
|
|
* @param string $url URL for redirect |
307
|
|
|
*/ |
308
|
1 |
|
protected function redirect(string $url) |
309
|
|
|
{ |
310
|
1 |
|
$this->headers['location'] = "Location: $url"; |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Disable rendering if redirecting |
314
|
|
|
*/ |
315
|
1 |
|
$this->render = false; |
316
|
1 |
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Get layout template |
320
|
|
|
* |
321
|
|
|
* @return string Layout template file path |
322
|
|
|
*/ |
323
|
1 |
|
protected function getLayoutTemplate(): string |
324
|
|
|
{ |
325
|
1 |
|
$layout = $this->layout; |
326
|
|
|
|
327
|
1 |
|
if (empty($layout)) { |
328
|
|
|
$layout = 'View/Layout/default'; |
329
|
|
|
} |
330
|
|
|
|
331
|
1 |
|
return "{$this->configuration->get('App.Path')}$layout.layout"; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Get view template |
336
|
|
|
* |
337
|
|
|
* @return string View template file path |
338
|
|
|
*/ |
339
|
2 |
|
protected function getViewTemplate(): string |
340
|
|
|
{ |
341
|
2 |
|
$view = $this->view; |
342
|
|
|
|
343
|
2 |
|
if (empty($view)) { |
344
|
1 |
|
$view = sprintf('View/%s/%s', $this->getShortName(), $this->action); |
345
|
|
|
} |
346
|
|
|
|
347
|
2 |
|
return "{$this->configuration->get('App.Path')}$view.view"; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Set response code |
352
|
|
|
* |
353
|
|
|
* Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error |
354
|
|
|
* |
355
|
|
|
* @param int $code HTTP response code |
356
|
|
|
* |
357
|
|
|
* @throws \InvalidArgumentException If unsupported code is provided |
358
|
|
|
*/ |
359
|
9 |
|
protected function setResponseCode(int $code) |
360
|
|
|
{ |
361
|
|
|
switch ($code) { |
362
|
|
|
/** |
363
|
|
|
* Success |
364
|
|
|
*/ |
365
|
9 |
|
case 200: |
366
|
1 |
|
$text = 'OK'; |
367
|
1 |
|
break; |
368
|
|
|
|
369
|
8 |
|
case 201: |
370
|
1 |
|
$text = 'Created'; |
371
|
1 |
|
break; |
372
|
|
|
|
373
|
7 |
|
case 202: |
374
|
1 |
|
$text = 'Accepted'; |
375
|
1 |
|
break; |
376
|
|
|
|
377
|
6 |
|
case 204: |
378
|
1 |
|
$text = 'No Content'; |
379
|
1 |
|
break; |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Client error |
383
|
|
|
*/ |
384
|
5 |
|
case 400: |
385
|
1 |
|
$text = 'Bad Request'; |
386
|
1 |
|
break; |
387
|
|
|
|
388
|
4 |
|
case 403: |
389
|
1 |
|
$text = 'Forbidden'; |
390
|
1 |
|
break; |
391
|
|
|
|
392
|
3 |
|
case 404: |
393
|
1 |
|
$text = 'Not Found'; |
394
|
1 |
|
break; |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Server error |
398
|
|
|
*/ |
399
|
2 |
|
case 500: |
400
|
1 |
|
$text = 'Internal Server Error'; |
401
|
1 |
|
break; |
402
|
|
|
|
403
|
|
|
default: |
404
|
1 |
|
throw new \InvalidArgumentException("HTTP status '$code' is not implemented"); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
/** |
408
|
|
|
* Set header |
409
|
|
|
*/ |
410
|
|
|
// @todo test that running response code multiple times only results in one response code header |
411
|
8 |
|
$this->headers['response_code'] = "HTTP/1.1 $code $text"; |
412
|
8 |
|
} |
413
|
|
|
} |
414
|
|
|
|
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.