Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
90 / 90
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
1 / 1
ServerRequest
100.00% covered (success)
100.00%
90 / 90
100.00% covered (success)
100.00%
23 / 23
40
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 normalizeFiles
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 createUploadedFileFromSpec
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 normalizeNestedFileSpec
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 fromGlobals
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getUriFromGlobals
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getServerParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUploadedFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withUploadedFiles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getCookieParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withCookieParams
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getQueryParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withQueryParams
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getParsedBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withParsedBody
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getAttributes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAttribute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 withAttribute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 withoutAttribute
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getCookie
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasCookie
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelativePath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getBase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Tal;
4
5use GuzzleHttp\Psr7\CachingStream;
6use GuzzleHttp\Psr7\LazyOpenStream;
7use GuzzleHttp\Psr7\UploadedFile;
8use GuzzleHttp\Psr7\Uri;
9use InvalidArgumentException;
10use Psr\Http\Message\RequestInterface;
11use Psr\Http\Message\ServerRequestInterface as PsrServerRequestInterface;
12use Psr\Http\Message\UriInterface;
13use Psr\Http\Message\StreamInterface;
14use Psr\Http\Message\UploadedFileInterface;
15use Tal\Psr7Extended\ServerRequestInterface;
16
17/**
18 * Server-side HTTP request
19 *
20 * Extends the Request definition to add methods for accessing incoming data,
21 * specifically server parameters, cookies, matched path parameters, query
22 * string arguments, body parameters, and upload file information.
23 *
24 * "Attributes" are discovered via decomposing the request (and usually
25 * specifically the URI path), and typically will be injected by the application.
26 *
27 * Requests are considered immutable; all methods that might change state are
28 * implemented such that they retain the internal state of the current
29 * message and return a new instance that contains the changed state.
30 */
31class ServerRequest extends Request implements ServerRequestInterface
32{
33    /**
34     * @var array
35     */
36    protected $attributes = [];
37
38    /**
39     * @var array
40     */
41    protected $cookieParams = [];
42
43    /**
44     * @var null|array|object
45     */
46    protected $parsedBody;
47
48    /**
49     * @var array
50     */
51    protected $queryParams = [];
52
53    /**
54     * @var array
55     */
56    protected $serverParams;
57
58    /**
59     * @var array
60     */
61    protected $uploadedFiles = [];
62
63    /**
64     * @param string                               $method       HTTP method
65     * @param string|UriInterface                  $uri          URI
66     * @param array                                $headers      Request headers
67     * @param string|null|resource|StreamInterface $body         Request body
68     * @param string                               $version      Protocol version
69     * @param array                                $serverParams Typically the $_SERVER superglobal
70     */
71    public function __construct(
72        $method,
73        $uri,
74        array $headers = [],
75        $body = null,
76        $version = '1.1',
77        array $serverParams = []
78    ) {
79        $this->serverParams = $serverParams;
80
81        parent::__construct($method, $uri, $headers, $body, $version);
82    }
83
84    /**
85     * Return an UploadedFile instance array.
86     *
87     * @param array $files A array which respect $_FILES structure
88     * @throws InvalidArgumentException for unrecognized values
89     * @return array
90     */
91    public static function normalizeFiles(array $files)
92    {
93        $normalized = [];
94
95        foreach ($files as $key => $value) {
96            if ($value instanceof UploadedFileInterface) {
97                $normalized[$key] = $value;
98            } elseif (is_array($value) && isset($value['tmp_name'])) {
99                $normalized[$key] = self::createUploadedFileFromSpec($value);
100            } elseif (is_array($value)) {
101                $normalized[$key] = self::normalizeFiles($value);
102                continue;
103            } else {
104                throw new InvalidArgumentException('Invalid value in files specification');
105            }
106        }
107
108        return $normalized;
109    }
110
111    /**
112     * Create and return an UploadedFile instance from a $_FILES specification.
113     *
114     * If the specification represents an array of values, this method will
115     * delegate to normalizeNestedFileSpec() and return that return value.
116     *
117     * @param array $value $_FILES struct
118     * @return array|UploadedFileInterface
119     */
120    protected static function createUploadedFileFromSpec(array $value)
121    {
122        if (is_array($value['tmp_name'])) {
123            return self::normalizeNestedFileSpec($value);
124        }
125
126        return new UploadedFile(
127            $value['tmp_name'],
128            (int) $value['size'],
129            (int) $value['error'],
130            $value['name'],
131            $value['type']
132        );
133    }
134
135    /**
136     * Normalize an array of file specifications.
137     *
138     * Loops through all nested files and returns a normalized array of
139     * UploadedFileInterface instances.
140     *
141     * @param array $files
142     * @return UploadedFileInterface[]
143     */
144    protected static function normalizeNestedFileSpec(array $files = [])
145    {
146        $normalizedFiles = [];
147
148        foreach (array_keys($files['tmp_name']) as $key) {
149            $spec = [
150                'tmp_name' => $files['tmp_name'][$key],
151                'size'     => $files['size'][$key],
152                'error'    => $files['error'][$key],
153                'name'     => $files['name'][$key],
154                'type'     => $files['type'][$key],
155            ];
156            $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
157        }
158
159        return $normalizedFiles;
160    }
161
162    /**
163     * Return a ServerRequestInterface populated with superglobals:
164     * $_GET
165     * $_POST
166     * $_COOKIE
167     * $_FILES
168     * $_SERVER
169     *
170     * @return ServerRequestInterface
171     */
172    public static function fromGlobals()
173    {
174        $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
175        $headers = function_exists('getallheaders') ? getallheaders() : [];
176        $uri = self::getUriFromGlobals();
177        $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
178        $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1';
179
180        $serverRequest = new static($method, $uri, $headers, $body, $protocol, $_SERVER);
181
182        return $serverRequest
183            ->withCookieParams($_COOKIE)
184            ->withQueryParams($_GET)
185            ->withParsedBody($_POST)
186            ->withUploadedFiles(self::normalizeFiles($_FILES));
187    }
188
189    /**
190     * Get a Uri populated with values from $_SERVER.
191     *
192     * @return UriInterface
193     */
194    public static function getUriFromGlobals()
195    {
196        $uri = new Uri('');
197
198        $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http')
199            ->withHost(Server::getHost() ?? '')
200            ->withPort(Server::getPort());
201
202        $path = Server::getPath();
203        if ($path !== null) {
204            $uri = $uri->withPath($path);
205        }
206
207        $query = Server::getQuery();
208        if ($query !== null) {
209            $uri = $uri->withQuery($query);
210        }
211
212        return $uri;
213    }
214
215
216    /**
217     * {@inheritdoc}
218     */
219    public function getServerParams(): array
220    {
221        return $this->serverParams;
222    }
223
224    /**
225     * {@inheritdoc}
226     */
227    public function getUploadedFiles(): array
228    {
229        return $this->uploadedFiles;
230    }
231
232    /**
233     * {@inheritdoc}
234     */
235    public function withUploadedFiles(array $uploadedFiles): PsrServerRequestInterface
236    {
237        $new = clone $this;
238        $new->uploadedFiles = $uploadedFiles;
239
240        return $new;
241    }
242
243    /**
244     * {@inheritdoc}
245     */
246    public function getCookieParams(): array
247    {
248        return $this->cookieParams;
249    }
250
251    /**
252     * {@inheritdoc}
253     */
254    public function withCookieParams(array $cookies): PsrServerRequestInterface
255    {
256        $new = clone $this;
257        $new->cookieParams = $cookies;
258
259        return $new;
260    }
261
262    /**
263     * {@inheritdoc}
264     */
265    public function getQueryParams(): array
266    {
267        return $this->queryParams;
268    }
269
270    /**
271     * {@inheritdoc}
272     */
273    public function withQueryParams(array $query): PsrServerRequestInterface
274    {
275        $new = clone $this;
276        $new->queryParams = $query;
277
278        return $new;
279    }
280
281    /**
282     * {@inheritdoc}
283     */
284    public function getParsedBody()
285    {
286        return $this->parsedBody;
287    }
288
289    /**
290     * {@inheritdoc}
291     */
292    public function withParsedBody($data): PsrServerRequestInterface
293    {
294        $new = clone $this;
295        $new->parsedBody = $data;
296
297        return $new;
298    }
299
300    /**
301     * {@inheritdoc}
302     */
303    public function getAttributes(): array
304    {
305        return $this->attributes;
306    }
307
308    /**
309     * {@inheritdoc}
310     */
311    public function getAttribute(string $attribute, $default = null)
312    {
313        if (false === array_key_exists($attribute, $this->attributes)) {
314            return $default;
315        }
316
317        return $this->attributes[$attribute];
318    }
319
320    /**
321     * {@inheritdoc}
322     */
323    public function withAttribute(string $attribute, $value): PsrServerRequestInterface
324    {
325        $new = clone $this;
326        $new->attributes[$attribute] = $value;
327
328        return $new;
329    }
330
331    /**
332     * {@inheritdoc}
333     */
334    public function withoutAttribute(string $attribute): PsrServerRequestInterface
335    {
336        if (false === array_key_exists($attribute, $this->attributes)) {
337            return $this;
338        }
339
340        $new = clone $this;
341        unset($new->attributes[$attribute]);
342
343        return $new;
344    }
345
346    /**
347     * Get cookie by name
348     *
349     * @param string $name
350     * @param mixed $default
351     * @return mixed
352     */
353    public function getCookie(string $name, $default = null)
354    {
355        return $this->getCookieParams()[$name] ?? $default;
356    }
357
358    /**
359     * Check if cookie is set
360     *
361     * @param string $name
362     * @return bool
363     */
364    public function hasCookie(string $name): bool
365    {
366        return isset($this->getCookieParams()[$name]);
367    }
368
369    /**
370     * Get the relative path to $base.
371     *
372     * If $base is not given it will be determined by $_SERVER['SCRIPT_NAME'].
373     *
374     * e. g. The path is `/shop/products/foo` and the SCRIPT_NAME is `/shop/index.php` the result would be
375     * `/products/foo`.
376     *
377     * @param string $base The base where you want to start without slash
378     * @return string The path including the first slash
379     */
380    public function getRelativePath(string $base = null): string
381    {
382        if ($base === null) {
383            $base = $this->getBase();
384        }
385
386        return substr($this->getUri()->getPath(), strlen(rtrim($base, '/')));
387    }
388
389    /**
390     * Get the domain absolute path to your application.
391     *
392     * If your software is installed on a separate (sub-)domain this will return '/' otherwise the path to your
393     * php file.
394     *
395     * @return string
396     */
397    public function getBase(): string
398    {
399        return dirname($this->getServerParams()['SCRIPT_NAME'] ?? '/index.php');
400    }
401}