Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
Request
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
11 / 11
46
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 isHttpsRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isShellRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parsePath
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
17
 getPath
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getRequest
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getServer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPost
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getPayload
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActual
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 __set_state
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2
3namespace Miniframe\Core;
4
5/**
6 * Request object
7 *
8 * This class represents a HTTP(S) or shell request
9 * To get the actual request, use Request::getActual()
10 *
11 * Shell requests are automatically translated to a path and request data. These requests are the same:
12 * - /foo/bar?page=1
13 * - php index.php foo bar --page=1
14 */
15class Request
16{
17    /**
18     * A list of all request ($_GET) data
19     *
20     * @var array<string, string|string[]>
21     */
22    private $request_data = array();
23
24    /**
25     * A list of all shell arguments (parsed from $_SERVER['argv'])
26     *
27     * @var array<int|string, mixed>
28     */
29    private $shell_arguments = array();
30
31    /**
32     * A list of all posted ($_POST) data
33     *
34     * @var array<string, string|string[]>
35     */
36    private $post_data = array();
37
38    /**
39     * Payload of the request
40     *
41     * @var null|string
42     */
43    private $payload_data = null;
44
45    /**
46     * A list of all posted ($_FILES) files
47     *
48     * @var  mixed[]
49     * @todo Write a get method for posted files
50     */
51    private $files_data = array();
52
53    /**
54     * A list of all server ($_SERVER) data
55     *
56     * @var array<string, string|string[]>
57     */
58    private $server_data = array();
59
60    /**
61     * Path of the request
62     *
63     * @var string[]
64     */
65    private $path = array();
66
67    /**
68     * Creates a new request
69     *
70     * @param array<string, string|string[]> $server_data  A list of all server ($_SERVER) data.
71     * @param array<string, string|string[]> $request_data A list of all request ($_GET) data.
72     * @param array<string, string|string[]> $post_data    A list of all posted ($_POST) data.
73     * @param mixed[]                        $files_data   A list of all posted ($_FILES) files.
74     * @param string|null                    $payload_data The full POST payload.
75     *
76     * @throws \Exception An exception can be thrown when no REQUEST_URI nor argv is populated.
77     */
78    public function __construct(
79        array $server_data = array(),
80        array $request_data = array(),
81        array $post_data = array(),
82        array $files_data = array(),
83        string $payload_data = null
84    ) {
85        $this->server_data = $server_data;
86        $this->request_data = $request_data;
87        $this->post_data = $post_data;
88        $this->files_data = $files_data;
89        $this->payload_data = $payload_data;
90
91        // Parses the path
92        $this->parsePath();
93    }
94
95    /**
96     * Returns true when the request is done over HTTPS
97     *
98     * @return boolean
99     */
100    public function isHttpsRequest(): bool
101    {
102        return isset($this->server_data['HTTPS']);
103    }
104
105    /**
106     * Returns true when the request is done from the shell
107     *
108     * @return boolean
109     */
110    public function isShellRequest(): bool
111    {
112        return php_sapi_name() == 'cli';
113    }
114
115    /**
116     * Calculates the path and sets it to the 'path' variable.
117     *
118     * When on a shell, also populates the request_data variable with shell arguments.
119     *
120     * @return void
121     * @throws \Exception An exception can be thrown when no REQUEST_URI nor argv is populated.
122     */
123    private function parsePath(): void
124    {
125        // Web request
126        if (
127            isset($this->server_data['REQUEST_URI'])
128            && is_string($this->server_data['REQUEST_URI'])
129            && substr($this->server_data['REQUEST_URI'], 0, 1) == '/'
130        ) {
131            list($request) = explode('?', $this->server_data['REQUEST_URI'], 2);
132            if (trim($request, '/') === '') {
133                $this->path = array();
134                return;
135            } else {
136                $this->path = explode('/', trim($request, '/'));
137                return;
138            }
139        }
140
141        // Parses shell requests
142        if (isset($this->server_data['argv']) && is_array($this->server_data['argv'])) {
143            $destination = null;
144            $argument_values = $this->server_data['argv'];
145            array_shift($argument_values); // Remove the call itself
146            foreach ($argument_values as $argument_value) {
147                if (substr($argument_value, 0, 2) === '--') {
148                    // A parameter with two minus signs, remove one so we catch -- and - in the same code
149                    $argument_value = substr($argument_value, 1);
150                }
151                if (substr($argument_value, 0, 1) === '-') {
152                    // A parameter has been specified
153                    $destination = substr($argument_value, 1);
154                    if (strpos($destination, '=') !== false) {
155                        list($destination, $argument_value) = explode("=", $destination, 2);
156                    } else {
157                        $argument_value = null;
158                    }
159                }
160
161                if ($destination === null) {
162                    // Before any parameter will be considered as path
163                    array_push($this->path, $argument_value);
164                } elseif (!isset($this->shell_arguments[$destination])) {
165                    // First time a parameter is mentioned, make sure it's set
166                    $this->shell_arguments[$destination] = $argument_value ? $argument_value : true;
167                } elseif ($argument_value === null) {
168                    // Parameter is called again, but no new data yet
169                    continue;
170                } elseif ($this->shell_arguments[$destination] === true) {
171                    // First time text is added for a parameter
172                    $this->shell_arguments[$destination] = $argument_value;
173                } elseif (!is_array($this->shell_arguments[$destination])) {
174                    // Parameter is mentioned for the second time, convert to array
175                    $this->shell_arguments[$destination] = array(
176                        $this->shell_arguments[$destination],
177                        $argument_value
178                    );
179                } else {
180                    // Parameter is mentioned even more, just keep on appending data
181                    array_push($this->shell_arguments[$destination], $argument_value);
182                }
183            }
184            return;
185        }
186
187        throw new \RuntimeException('Validate if $_SERVER["REQUEST_URI"] or $_SERVER["argv"] is set properly');
188    }
189
190    /**
191     * Returns the path of the web request as array when no index is specified,
192     * or a specific path item when an index is specified.
193     *
194     * Returns null when the index is specified but there are not that many path items.
195     * Returns a string when the index is specified and there are enough path items.
196     * Returns an array if no index is specified.
197     *
198     * @param integer|null $index The index of the required path.
199     *
200     * @return ($index is null ? string[] : string|null)
201     */
202    public function getPath(int $index = null)
203    {
204        if ($index === null) {
205            return $this->path;
206        }
207
208        // When the index is negative one, and we're in a shell, return the called command
209        if ($index === -1 && $this->isShellRequest()) {
210            return $this->server_data['argv'][0] ?? null;
211        }
212
213        return $this->path[$index] ?? null;
214    }
215
216    /**
217     * Returns all request values when no key is specified, or a specific request value when a key is specified.
218     *
219     * Returns null when the key is specified but no value is found
220     * Returns a string when the key is specified and one value is found
221     * Returns an array if no key is specified, or the key has multiple values
222     *
223     * @param string|null $key The request value to request.
224     *
225     * @return ($key is null ? array<string, string|string[]> : string|string[]|null)
226     */
227    public function getRequest(?string $key = null)
228    {
229        // Defines the request array
230        if ($this->isShellRequest()) {
231            $var = 'shell_arguments';
232        } else {
233            $var = 'request_data';
234        }
235
236        if ($key === null) {
237            return $this->{$var};
238        }
239        return $this->{$var}[$key] ?? null;
240    }
241
242    /**
243     * Returns all server values when no key is specified, or a specific server value when a key is specified.
244     *
245     * Returns null when the key is specified but no value is found
246     * Returns a string when the key is specified and one value is found
247     * Returns an array if no key is specified, or the key has multiple values
248     *
249     * @param string|null $key The request value to request.
250     *
251     * @return ($key is null ? array<string, string|string[]> : string|string[]|null)
252     */
253    public function getServer(?string $key = null)
254    {
255        if ($key === null) {
256            return $this->server_data;
257        }
258
259        return $this->server_data[$key] ?? null;
260    }
261
262    /**
263     * Returns all posted values when no key is specified, or a specific posted value when a key is specified.
264     *
265     * Returns null when the key is specified but no value is found
266     * Returns a string when the key is specified and one value is found
267     * Returns an array if no key is specified, or the key has multiple values
268     *
269     * @param string|null $key The request value to request.
270     *
271     * @return ($key is null ? array<string, string|string[]> : string|string[]|null)
272     */
273    public function getPost(?string $key = null)
274    {
275        if (isset($key)) {
276            return isset($this->post_data[$key]) ? $this->post_data[$key] : null;
277        }
278        return $this->post_data;
279    }
280
281    /**
282     * Returns the posted payload data as string. When no data exists, null will be returned.
283     *
284     * @return string|null
285     */
286    public function getPayload(): ?string
287    {
288        return $this->payload_data;
289    }
290
291    /**
292     * Returns a Request object based on the actual web request
293     *
294     * @return Request
295     */
296    public static function getActual(): self
297    {
298        return static::__set_state([
299            'server_data'  => $_SERVER,
300            'request_data' => $_GET,
301            'post_data'    => $_POST,
302            'files_data'   => $_FILES,
303            'payload_data' => file_get_contents('php://input') ?: null,
304        ]);
305    }
306
307    /**
308     * Magic method; sets a request object based on a specific state
309     *
310     * @param array<string, mixed> $data The actual state.
311     *
312     * @return Request
313     * @throws \Exception An exception can be thrown when no REQUEST_URI nor argv is populated.
314     * @see    var_export()
315     */
316    public static function __set_state(array $data): self
317    {
318        return new self(
319            isset($data['server_data']) && is_array($data['server_data']) ? $data['server_data'] : array(),
320            isset($data['request_data']) && is_array($data['request_data']) ? $data['request_data'] : array(),
321            isset($data['post_data']) && is_array($data['post_data']) ? $data['post_data'] : array(),
322            isset($data['files_data']) && is_array($data['files_data']) ? $data['files_data'] : array(),
323            isset($data['payload_data']) && is_string($data['payload_data']) ? $data['payload_data'] : null
324        );
325    }
326}