Completed
Push — feature/controller ( 08e254...534c3a )
by René
09:48
created

Controller::setAction()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 25
ccs 10
cts 10
cp 1
rs 8.439
cc 6
eloc 9
nc 5
nop 1
crap 6
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\Model\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 login 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
     * @return string Controller name without namespace
93
     */
94
    public function getShortName(): string
95
    {
96
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
97
    }
98
99
    /**
100
     * @param string $action Controller action
101
     *
102
     * @throws ControllerActionNonexistentException
103
     * @throws ControllerActionPrivateInsufficientAuthenticationException
104
     * @throws ControllerActionProtectedInsufficientAuthenticationException
105
     */
106 4
    public function setAction(string $action)
107
    {
108
        /**
109
         * Check if method exists and that access has been defined
110
         */
111 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
112 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
113
        }
114
115
        /**
116
         * Check controller action access level if user is not authenticated
117
         */
118 3
        if (!$this->user) {
119 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
120 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
121 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
122 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
123
            }
124
        }
125
126
        /**
127
         * Set controller action
128
         */
129 1
        $this->action = $action;
130 1
    }
131
132
    /**
133
     * Call action
134
     *
135
     * @return array<string,array|string> Headers and output if render is enabled, otherwise FALSE
136
     *
137
     * @throws \LogicException If controller action is not set
138
     */
139
    public function callAction(): array
140
    {
141
        if (!isset($this->action)) {
142
            throw new \LogicException('Controller action must be set before being called');
143
        }
144
145
        /**
146
         * Before controller action hook
147
         */
148
        $this->beforeAction();
149
150
        /**
151
         * Call controller action
152
         */
153
        $action = $this->action;
154
155
        $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...
156
157
        /**
158
         * After controller action hook
159
         */
160
        $this->afterAction();
161
162
        /**
163
         * Render view
164
         */
165
        if ($this->render) {
166
            $render = new HtmlRender($this->variables);
167
168
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
169
170
            return [
171
                'headers' => $this->headers,
172
                'output'  => $output
173
            ];
174
        }
175
176
        return [];
177
    }
178
179
    /**
180
     * Before controller action hook
181
     *
182
     * Called right before controller action is called
183
     */
184
    protected function beforeAction()
185
    {
186
        /**
187
         * Set New Relic transaction name
188
         */
189
        if (extension_loaded('newrelic')) {
190
            newrelic_name_transaction(sprintf('%s/%s', $this->getShortName(), $this->action));
191
        }
192
    }
193
194
    /**
195
     * After controller action hook
196
     *
197
     * Called right after controller action is called, but before rendering of the view
198
     */
199
    protected function afterAction()
200
    {
201
    }
202
203
    /**
204
     * Set view variable
205
     *
206
     * @param string $variable
207
     * @param mixed  $value
208
     */
209
    protected function set(string $variable, $value)
210
    {
211
        $this->variables[$variable] = $value;
212
    }
213
214
    /**
215
     * Get layout template
216
     *
217
     * @return string Layout template file path
218
     */
219
    protected function getLayoutTemplate(): string
220
    {
221
        $layout = $this->layout;
222
223
        if (empty($layout)) {
224
            $layout = 'View/Layout/default';
225
        }
226
227
        return "{$this->appPath}$layout.layout";
228
    }
229
230
    /**
231
     * Get view template
232
     *
233
     * @return string View template file path
234
     */
235 2
    protected function getViewTemplate(): string
236
    {
237 2
        $view = $this->view;
238
239 2
        if (empty($view)) {
240 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
241
        }
242
243 2
        return "{$this->appPath}$view.view";
244
    }
245
246
    /**
247
     * Set response code
248
     *
249
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
250
     *
251
     * @param int $code HTTP response code
252
     *
253
     * @throws \InvalidArgumentException If unsupported code is provided
254
     */
255
    protected function setResponseCode(int $code)
256
    {
257
        switch ($code) {
258
            case 200:
259
                $text = 'OK';
260
                break;
261
262
            case 403:
263
                $text = 'Forbidden';
264
                break;
265
266
            case 404:
267
                $text = 'Not Found';
268
                break;
269
270
            case 500:
271
                $text = 'Internal Server Error';
272
                break;
273
274
            default:
275
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
276
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
277
        }
278
279
        /**
280
         * Set header
281
         */
282
        // @todo test that running response code multiple times only results in one response code header
283
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
284
    }
285
286
    /**
287
     * @param \PDO      $pdo
288
     * @param string    $appPath
289
     * @param null|User $user
290
     */
291
    public function __construct(\PDO $pdo, string $appPath, User $user = null)
292
    {
293
        $this->pdo     = $pdo;
294
        $this->appPath = $appPath;
295
        $this->user    = $user;
296
    }
297
}
298