1 | <?php |
||
2 | |||
3 | /** |
||
4 | * It's free open-source software released under the MIT License. |
||
5 | * |
||
6 | * @author Anatoly Nekhay <[email protected]> |
||
7 | * @copyright Copyright (c) 2018, Anatoly Nekhay |
||
8 | * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE |
||
9 | * @link https://github.com/sunrise-php/http-router |
||
10 | */ |
||
11 | |||
12 | declare(strict_types=1); |
||
13 | |||
14 | namespace Sunrise\Http\Router; |
||
15 | |||
16 | use Generator; |
||
17 | use LogicException; |
||
18 | use Psr\Http\Message\ServerRequestInterface; |
||
19 | use Psr\Http\Message\StreamInterface; |
||
20 | use Psr\Http\Message\UriInterface; |
||
21 | use Sunrise\Coder\MediaTypeInterface; |
||
22 | use Sunrise\Http\Router\Dictionary\HeaderName; |
||
23 | use Sunrise\Http\Router\Helper\HeaderParser; |
||
24 | |||
25 | use function extension_loaded; |
||
26 | use function reset; |
||
27 | use function strcasecmp; |
||
28 | use function usort; |
||
29 | |||
30 | /** |
||
31 | * @since 3.0.0 |
||
32 | */ |
||
33 | final class ServerRequest implements ServerRequestInterface |
||
34 | { |
||
35 | 144 | public function __construct( |
|
36 | private ServerRequestInterface $request, |
||
37 | ) { |
||
38 | 144 | } |
|
39 | |||
40 | 144 | public static function create(ServerRequestInterface $request): self |
|
41 | { |
||
42 | 144 | return ($request instanceof self) ? $request : new self($request); |
|
43 | } |
||
44 | |||
45 | 2 | public function hasRoute(): bool |
|
46 | { |
||
47 | 2 | $route = $this->request->getAttribute(RouteInterface::class); |
|
48 | |||
49 | 2 | return $route instanceof RouteInterface; |
|
50 | } |
||
51 | |||
52 | /** |
||
53 | * @throws LogicException If the request doesn't contain a route. |
||
54 | */ |
||
55 | 38 | public function getRoute(): RouteInterface |
|
56 | { |
||
57 | 38 | $route = $this->request->getAttribute(RouteInterface::class); |
|
58 | |||
59 | 38 | if (! $route instanceof RouteInterface) { |
|
60 | // phpcs:ignore Generic.Files.LineLength.TooLong |
||
61 | 1 | throw new LogicException('At this level of the application, the request does not contain information about the requested route.'); |
|
62 | } |
||
63 | |||
64 | 37 | return $route; |
|
65 | } |
||
66 | |||
67 | 13 | public function getClientProducedMediaType(): ?MediaTypeInterface |
|
68 | { |
||
69 | 13 | $values = HeaderParser::parseHeader($this->request->getHeaderLine(HeaderName::CONTENT_TYPE)); |
|
70 | 13 | if ($values === []) { |
|
71 | 2 | return null; |
|
72 | } |
||
73 | |||
74 | 11 | [$identifier] = reset($values); |
|
75 | |||
76 | 11 | return new MediaType($identifier); |
|
77 | } |
||
78 | |||
79 | /** |
||
80 | * @return Generator<int, MediaTypeInterface> |
||
81 | */ |
||
82 | 26 | public function getClientConsumedMediaTypes(): Generator |
|
83 | { |
||
84 | 26 | $values = HeaderParser::parseHeader($this->request->getHeaderLine(HeaderName::ACCEPT)); |
|
85 | 26 | if ($values === []) { |
|
86 | 2 | return; |
|
87 | } |
||
88 | |||
89 | // https://github.com/php/php-src/blob/d9549d2ee215cb04aa5d2e3195c608d581fb272c/ext/standard/array.c#L900-L903 |
||
90 | 24 | usort($values, static fn(array $a, array $b): int => ( |
|
91 | 15 | (float) ($b[1]['q'] ?? '1') <=> (float) ($a[1]['q'] ?? '1') |
|
92 | 24 | )); |
|
93 | |||
94 | 24 | foreach ($values as [$identifier]) { |
|
95 | 24 | yield new MediaType($identifier); |
|
96 | } |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * @param T ...$serverProducedMediaTypes |
||
101 | * |
||
102 | * @return T|null |
||
103 | * |
||
104 | * @template T of MediaTypeInterface |
||
105 | */ |
||
106 | 27 | public function getClientPreferredMediaType(MediaTypeInterface ...$serverProducedMediaTypes): ?MediaTypeInterface |
|
107 | { |
||
108 | 27 | if ($serverProducedMediaTypes === []) { |
|
109 | 9 | return null; |
|
110 | } |
||
111 | |||
112 | 18 | foreach ($this->getClientConsumedMediaTypes() as $clientConsumedMediaType) { |
|
113 | 17 | foreach ($serverProducedMediaTypes as $serverProducedMediaType) { |
|
114 | if ( |
||
115 | 17 | strcasecmp( |
|
116 | 17 | $clientConsumedMediaType->getIdentifier(), |
|
117 | 17 | $serverProducedMediaType->getIdentifier(), |
|
118 | 17 | ) === 0 |
|
119 | ) { |
||
120 | 16 | return $serverProducedMediaType; |
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
121 | } |
||
122 | } |
||
123 | } |
||
124 | |||
125 | 2 | return null; |
|
126 | } |
||
127 | |||
128 | 13 | public function serverConsumesMediaType(MediaTypeInterface $clientProducedMediaType): bool |
|
129 | { |
||
130 | 13 | $serverConsumedMediaTypes = $this->getRoute()->getConsumedMediaTypes(); |
|
131 | |||
132 | 13 | foreach ($serverConsumedMediaTypes as $serverConsumedMediaType) { |
|
133 | if ( |
||
134 | 12 | strcasecmp( |
|
135 | 12 | $clientProducedMediaType->getIdentifier(), |
|
136 | 12 | $serverConsumedMediaType->getIdentifier(), |
|
137 | 12 | ) === 0 |
|
138 | ) { |
||
139 | 10 | return true; |
|
140 | } |
||
141 | } |
||
142 | |||
143 | 3 | return false; |
|
144 | } |
||
145 | |||
146 | /** |
||
147 | * @return Generator<int, LocaleInterface> |
||
148 | * |
||
149 | * @throws LogicException |
||
150 | */ |
||
151 | 23 | public function getClientConsumedLocales(): Generator |
|
152 | { |
||
153 | // @codeCoverageIgnoreStart |
||
154 | if (!extension_loaded('intl')) { |
||
155 | throw new LogicException( |
||
156 | 'To get the locales consumed by the client, ' . |
||
157 | 'the Intl (https://www.php.net/intl) extension must be installed.' |
||
158 | ); |
||
159 | } |
||
160 | // @codeCoverageIgnoreEnd |
||
161 | |||
162 | 23 | $values = HeaderParser::parseHeader($this->request->getHeaderLine(HeaderName::ACCEPT_LANGUAGE)); |
|
163 | 23 | if ($values === []) { |
|
164 | 2 | return; |
|
165 | } |
||
166 | |||
167 | // https://github.com/php/php-src/blob/d9549d2ee215cb04aa5d2e3195c608d581fb272c/ext/standard/array.c#L900-L903 |
||
168 | 21 | usort($values, static fn(array $a, array $b): int => ( |
|
169 | 16 | (float) ($b[1]['q'] ?? '1') <=> (float) ($a[1]['q'] ?? '1') |
|
170 | 21 | )); |
|
171 | |||
172 | 21 | foreach ($values as [$identifier]) { |
|
173 | /** @var array{language?: string, region?: string}|null $subtags */ |
||
174 | 21 | $subtags = \Locale::parseLocale($identifier); |
|
175 | |||
176 | 21 | if (isset($subtags['language'])) { |
|
177 | 20 | yield new Locale($subtags['language'], $subtags['region'] ?? null); |
|
178 | } |
||
179 | } |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * @param T ...$serverProducedLanguages |
||
184 | * |
||
185 | * @return T|null |
||
186 | * |
||
187 | * @template T of LanguageInterface |
||
188 | */ |
||
189 | 20 | public function getClientPreferredLanguage(LanguageInterface ...$serverProducedLanguages): ?LanguageInterface |
|
190 | { |
||
191 | 20 | if ($serverProducedLanguages === []) { |
|
192 | 7 | return null; |
|
193 | } |
||
194 | |||
195 | 13 | foreach ($this->getClientConsumedLocales() as $clientConsumedLocale) { |
|
196 | 12 | foreach ($serverProducedLanguages as $serverProducedLanguage) { |
|
197 | 12 | if ($clientConsumedLocale->getLanguageCode() === $serverProducedLanguage->getCode()) { |
|
198 | 11 | return $serverProducedLanguage; |
|
0 ignored issues
–
show
|
|||
199 | } |
||
200 | } |
||
201 | } |
||
202 | |||
203 | 2 | return null; |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * @inheritDoc |
||
208 | */ |
||
209 | 1 | public function getProtocolVersion(): string |
|
210 | { |
||
211 | 1 | return $this->request->getProtocolVersion(); |
|
212 | } |
||
213 | |||
214 | /** |
||
215 | * @inheritDoc |
||
216 | */ |
||
217 | 1 | public function withProtocolVersion($version): self |
|
218 | { |
||
219 | 1 | $clone = clone $this; |
|
220 | 1 | $clone->request = $this->request->withProtocolVersion($version); |
|
221 | |||
222 | 1 | return $clone; |
|
223 | } |
||
224 | |||
225 | /** |
||
226 | * @inheritDoc |
||
227 | */ |
||
228 | 1 | public function getHeaders(): array |
|
229 | { |
||
230 | 1 | return $this->request->getHeaders(); |
|
231 | } |
||
232 | |||
233 | /** |
||
234 | * @inheritDoc |
||
235 | */ |
||
236 | 2 | public function hasHeader($name): bool |
|
237 | { |
||
238 | 2 | return $this->request->hasHeader($name); |
|
239 | } |
||
240 | |||
241 | /** |
||
242 | * @inheritDoc |
||
243 | */ |
||
244 | 1 | public function getHeader($name): array |
|
245 | { |
||
246 | 1 | return $this->request->getHeader($name); |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * @inheritDoc |
||
251 | */ |
||
252 | 1 | public function getHeaderLine($name): string |
|
253 | { |
||
254 | 1 | return $this->request->getHeaderLine($name); |
|
255 | } |
||
256 | |||
257 | /** |
||
258 | * @inheritDoc |
||
259 | */ |
||
260 | 1 | public function withHeader($name, $value): self |
|
261 | { |
||
262 | 1 | $clone = clone $this; |
|
263 | 1 | $clone->request = $this->request->withHeader($name, $value); |
|
264 | |||
265 | 1 | return $clone; |
|
266 | } |
||
267 | |||
268 | /** |
||
269 | * @inheritDoc |
||
270 | */ |
||
271 | 1 | public function withAddedHeader($name, $value): self |
|
272 | { |
||
273 | 1 | $clone = clone $this; |
|
274 | 1 | $clone->request = $this->request->withAddedHeader($name, $value); |
|
275 | |||
276 | 1 | return $clone; |
|
277 | } |
||
278 | |||
279 | /** |
||
280 | * @inheritDoc |
||
281 | */ |
||
282 | 1 | public function withoutHeader($name): self |
|
283 | { |
||
284 | 1 | $clone = clone $this; |
|
285 | 1 | $clone->request = $this->request->withoutHeader($name); |
|
286 | |||
287 | 1 | return $clone; |
|
288 | } |
||
289 | |||
290 | /** |
||
291 | * @inheritDoc |
||
292 | */ |
||
293 | 1 | public function getBody(): StreamInterface |
|
294 | { |
||
295 | 1 | return $this->request->getBody(); |
|
296 | } |
||
297 | |||
298 | /** |
||
299 | * @inheritDoc |
||
300 | */ |
||
301 | 1 | public function withBody(StreamInterface $body): self |
|
302 | { |
||
303 | 1 | $clone = clone $this; |
|
304 | 1 | $clone->request = $this->request->withBody($body); |
|
305 | |||
306 | 1 | return $clone; |
|
307 | } |
||
308 | |||
309 | /** |
||
310 | * @inheritDoc |
||
311 | */ |
||
312 | 1 | public function getMethod(): string |
|
313 | { |
||
314 | 1 | return $this->request->getMethod(); |
|
315 | } |
||
316 | |||
317 | /** |
||
318 | * @inheritDoc |
||
319 | */ |
||
320 | 1 | public function withMethod($method): self |
|
321 | { |
||
322 | 1 | $clone = clone $this; |
|
323 | 1 | $clone->request = $this->request->withMethod($method); |
|
324 | |||
325 | 1 | return $clone; |
|
326 | } |
||
327 | |||
328 | /** |
||
329 | * @inheritDoc |
||
330 | */ |
||
331 | 1 | public function getUri(): UriInterface |
|
332 | { |
||
333 | 1 | return $this->request->getUri(); |
|
334 | } |
||
335 | |||
336 | /** |
||
337 | * @inheritDoc |
||
338 | */ |
||
339 | 1 | public function withUri(UriInterface $uri, $preserveHost = false): self |
|
340 | { |
||
341 | 1 | $clone = clone $this; |
|
342 | 1 | $clone->request = $this->request->withUri($uri, $preserveHost); |
|
343 | |||
344 | 1 | return $clone; |
|
345 | } |
||
346 | |||
347 | /** |
||
348 | * @inheritDoc |
||
349 | */ |
||
350 | 1 | public function getRequestTarget(): string |
|
351 | { |
||
352 | 1 | return $this->request->getRequestTarget(); |
|
353 | } |
||
354 | |||
355 | /** |
||
356 | * @inheritDoc |
||
357 | */ |
||
358 | 1 | public function withRequestTarget($requestTarget): self |
|
359 | { |
||
360 | 1 | $clone = clone $this; |
|
361 | 1 | $clone->request = $this->request->withRequestTarget($requestTarget); |
|
362 | |||
363 | 1 | return $clone; |
|
364 | } |
||
365 | |||
366 | /** |
||
367 | * {@inheritDoc} |
||
368 | * |
||
369 | * @return array<array-key, mixed> |
||
0 ignored issues
–
show
|
|||
370 | */ |
||
371 | 1 | public function getServerParams(): array |
|
372 | { |
||
373 | 1 | return $this->request->getServerParams(); |
|
374 | } |
||
375 | |||
376 | /** |
||
377 | * {@inheritDoc} |
||
378 | * |
||
379 | * @return array<array-key, mixed> |
||
0 ignored issues
–
show
|
|||
380 | */ |
||
381 | 1 | public function getQueryParams(): array |
|
382 | { |
||
383 | 1 | return $this->request->getQueryParams(); |
|
384 | } |
||
385 | |||
386 | /** |
||
387 | * {@inheritDoc} |
||
388 | * |
||
389 | * @param array<array-key, mixed> $query |
||
0 ignored issues
–
show
|
|||
390 | * |
||
391 | * @return static |
||
392 | */ |
||
393 | 1 | public function withQueryParams(array $query): self |
|
394 | { |
||
395 | 1 | $clone = clone $this; |
|
396 | 1 | $clone->request = $this->request->withQueryParams($query); |
|
397 | |||
398 | 1 | return $clone; |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * {@inheritDoc} |
||
403 | * |
||
404 | * @return array<array-key, mixed> |
||
0 ignored issues
–
show
|
|||
405 | */ |
||
406 | 15 | public function getCookieParams(): array |
|
407 | { |
||
408 | 15 | return $this->request->getCookieParams(); |
|
409 | } |
||
410 | |||
411 | /** |
||
412 | * {@inheritDoc} |
||
413 | * |
||
414 | * @param array<array-key, mixed> $cookies |
||
0 ignored issues
–
show
|
|||
415 | * |
||
416 | * @return static |
||
417 | */ |
||
418 | 1 | public function withCookieParams(array $cookies): self |
|
419 | { |
||
420 | 1 | $clone = clone $this; |
|
421 | 1 | $clone->request = $this->request->withCookieParams($cookies); |
|
422 | |||
423 | 1 | return $clone; |
|
424 | } |
||
425 | |||
426 | /** |
||
427 | * {@inheritDoc} |
||
428 | * |
||
429 | * @return array<array-key, mixed> |
||
0 ignored issues
–
show
|
|||
430 | */ |
||
431 | 1 | public function getUploadedFiles(): array |
|
432 | { |
||
433 | 1 | return $this->request->getUploadedFiles(); |
|
434 | } |
||
435 | |||
436 | /** |
||
437 | * {@inheritDoc} |
||
438 | * |
||
439 | * @param array<array-key, mixed> $uploadedFiles |
||
0 ignored issues
–
show
|
|||
440 | * |
||
441 | * @return static |
||
442 | */ |
||
443 | 1 | public function withUploadedFiles(array $uploadedFiles): self |
|
444 | { |
||
445 | 1 | $clone = clone $this; |
|
446 | 1 | $clone->request = $this->request->withUploadedFiles($uploadedFiles); |
|
447 | |||
448 | 1 | return $clone; |
|
449 | } |
||
450 | |||
451 | /** |
||
452 | * {@inheritDoc} |
||
453 | * |
||
454 | * @return array<array-key, mixed>|object|null |
||
0 ignored issues
–
show
|
|||
455 | */ |
||
456 | 1 | public function getParsedBody(): mixed |
|
457 | { |
||
458 | 1 | return $this->request->getParsedBody(); |
|
459 | } |
||
460 | |||
461 | /** |
||
462 | * {@inheritDoc} |
||
463 | * |
||
464 | * @param array<array-key, mixed>|object|null $data |
||
0 ignored issues
–
show
|
|||
465 | * |
||
466 | * @return static |
||
467 | */ |
||
468 | 1 | public function withParsedBody($data): self |
|
469 | { |
||
470 | 1 | $clone = clone $this; |
|
471 | 1 | $clone->request = $this->request->withParsedBody($data); |
|
472 | |||
473 | 1 | return $clone; |
|
474 | } |
||
475 | |||
476 | /** |
||
477 | * {@inheritDoc} |
||
478 | * |
||
479 | * @return array<array-key, mixed> |
||
0 ignored issues
–
show
|
|||
480 | */ |
||
481 | 1 | public function getAttributes(): array |
|
482 | { |
||
483 | 1 | return $this->request->getAttributes(); |
|
484 | } |
||
485 | |||
486 | /** |
||
487 | * {@inheritDoc} |
||
488 | * |
||
489 | * @param string $name |
||
490 | * @param mixed $default |
||
491 | * |
||
492 | * @return mixed |
||
493 | */ |
||
494 | 1 | public function getAttribute($name, $default = null): mixed |
|
495 | { |
||
496 | 1 | return $this->request->getAttribute($name, $default); |
|
497 | } |
||
498 | |||
499 | /** |
||
500 | * {@inheritDoc} |
||
501 | * |
||
502 | * @param string $name |
||
503 | * @param mixed $value |
||
504 | * |
||
505 | * @return static |
||
506 | */ |
||
507 | 1 | public function withAttribute($name, $value): self |
|
508 | { |
||
509 | 1 | $clone = clone $this; |
|
510 | 1 | $clone->request = $this->request->withAttribute($name, $value); |
|
511 | |||
512 | 1 | return $clone; |
|
513 | } |
||
514 | |||
515 | /** |
||
516 | * {@inheritDoc} |
||
517 | * |
||
518 | * @param string $name |
||
519 | * |
||
520 | * @return static |
||
521 | */ |
||
522 | 1 | public function withoutAttribute($name): self |
|
523 | { |
||
524 | 1 | $clone = clone $this; |
|
525 | 1 | $clone->request = $this->request->withoutAttribute($name); |
|
526 | |||
527 | 1 | return $clone; |
|
528 | } |
||
529 | } |
||
530 |