Completed
Push — feature/controller ( 5a6415...9e7a26 )
by René
07:43 queued 05:40
created

Controller::set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

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 4
ccs 0
cts 3
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 2
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\Network\Request;
11
use Zortje\MVC\Storage\Cookie\Cookie;
12
use Zortje\MVC\User\User;
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 Cookie
57
     */
58
    protected $cookie;
59
60
    /**
61
     * @var User|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 array Headers for output
92
     *
93
     * @todo JSON content type: `Content-Type: application/javascript; charset=utf-8`
94
     */
95
    protected $headers = [
96
        'content-type' => 'Content-Type: text/html; charset=utf-8'
97
    ];
98
99
    /**
100
     * Controller constructor.
101
     *
102
     * @param \PDO          $pdo
103
     * @param Configuration $configuration
104
     * @param Cookie        $cookie
105
     * @param User|null     $user
106
     */
107 1 View Code Duplication
    public function __construct(\PDO $pdo, Configuration $configuration, Cookie $cookie, 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...
108
    {
109 1
        $this->pdo           = $pdo;
110 1
        $this->configuration = $configuration;
111 1
        $this->cookie        = $cookie;
112 1
        $this->user          = $user;
113 1
    }
114
115
    /**
116
     * @return string Controller name without namespace
117
     */
118 1
    public function getShortName(): string
119
    {
120 1
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
121
    }
122
123
    /**
124
     * @param string $action Controller action
125
     *
126
     * @throws ControllerActionNonexistentException
127
     * @throws ControllerActionPrivateInsufficientAuthenticationException
128
     * @throws ControllerActionProtectedInsufficientAuthenticationException
129
     */
130 4
    public function setAction(string $action)
131
    {
132
        /**
133
         * Check if method exists and that access has been defined
134
         */
135 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
136 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
137
        }
138
139
        /**
140
         * Check controller action access level if user is not authenticated
141
         */
142 3
        if (!$this->user) {
143 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
144 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
145 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
146 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
147
            }
148
        }
149
150
        /**
151
         * Set controller action
152
         */
153 1
        $this->action = $action;
154 1
    }
155
156
    /**
157
     * Call action
158
     *
159
     * @return array<string,array|string> Headers and output if render is enabled, otherwise FALSE
160
     *
161
     * @throws \LogicException If controller action is not set
162
     */
163
    public function callAction(): array
164
    {
165
        if (!isset($this->action)) {
166
            throw new \LogicException('Controller action must be set before being called');
167
        }
168
169
        /**
170
         * Before controller action hook
171
         */
172
        $this->beforeAction();
173
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
         * Render view
188
         */
189
        if ($this->render) {
190
            $render = new HtmlRender($this->variables);
191
192
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
193
194
            return [
195
                'headers'      => $this->headers,
196
                'cookie_token' => $this->cookie,
197
                'output'       => $output
198
            ];
199
        }
200
201
        return [];
202
    }
203
204
    /**
205
     * Before controller action hook
206
     *
207
     * Called right before controller action is called
208
     */
209
    protected function beforeAction()
210
    {
211
    }
212
213
    /**
214
     * After controller action hook
215
     *
216
     * Called right after controller action is called, but before rendering of the view
217
     */
218
    protected function afterAction()
219
    {
220
    }
221
222
    /**
223
     * Set view variable
224
     *
225
     * @param string $variable
226
     * @param mixed  $value
227
     */
228
    protected function set(string $variable, $value)
229
    {
230
        $this->variables[$variable] = $value;
231
    }
232
233
    /**
234
     * Get layout template
235
     *
236
     * @return string Layout template file path
237
     */
238 1
    protected function getLayoutTemplate(): string
239
    {
240 1
        $layout = $this->layout;
241
242 1
        if (empty($layout)) {
243
            $layout = 'View/Layout/default';
244
        }
245
246 1
        return "{$this->configuration->get('App.Path')}$layout.layout";
247
    }
248
249
    /**
250
     * Get view template
251
     *
252
     * @return string View template file path
253
     */
254 2
    protected function getViewTemplate(): string
255
    {
256 2
        $view = $this->view;
257
258 2
        if (empty($view)) {
259 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
260
        }
261
262 2
        return "{$this->configuration->get('App.Path')}$view.view";
263
    }
264
265
    /**
266
     * Set response code
267
     *
268
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
269
     *
270
     * @param int $code HTTP response code
271
     *
272
     * @throws \InvalidArgumentException If unsupported code is provided
273
     */
274 5
    protected function setResponseCode(int $code)
275
    {
276
        switch ($code) {
277 5
            case 200:
278 1
                $text = 'OK';
279 1
                break;
280
281 4
            case 403:
282 1
                $text = 'Forbidden';
283 1
                break;
284
285 3
            case 404:
286 1
                $text = 'Not Found';
287 1
                break;
288
289 2
            case 500:
290 1
                $text = 'Internal Server Error';
291 1
                break;
292
293
            default:
294 1
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
295
        }
296
297
        /**
298
         * Set header
299
         */
300
        // @todo test that running response code multiple times only results in one response code header
301 4
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
302 4
    }
303
}
304