Completed
Pull Request — master (#2)
by René
05:47 queued 03:39
created

Controller::callAction()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 39
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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