Completed
Push — feature/controller ( a8254a...da95d8 )
by René
02:13
created

Controller::getLayoutTemplate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

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