1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the Koded package. |
||
5 | * |
||
6 | * (c) Mihail Binev <[email protected]> |
||
7 | * |
||
8 | * Please view the LICENSE distributed with this source code |
||
9 | * for the full copyright and license information. |
||
10 | * |
||
11 | */ |
||
12 | |||
13 | namespace Koded\Http; |
||
14 | |||
15 | use Koded\Http\Interfaces\{HttpStatus, Request, Response}; |
||
16 | use Psr\Http\Message\{RequestInterface, UriInterface}; |
||
17 | use function Koded\Stdlib\json_serialize; |
||
18 | |||
19 | class ClientRequest implements RequestInterface, \JsonSerializable |
||
20 | { |
||
21 | use HeaderTrait, MessageTrait, JsonSerializeTrait; |
||
22 | |||
23 | const E_INVALID_REQUEST_TARGET = 'The request target is invalid, it contains whitespaces'; |
||
24 | const E_SAFE_METHODS_WITH_BODY = 'failed to open stream: you should not set the message body with safe HTTP methods'; |
||
25 | |||
26 | protected UriInterface $uri; |
||
27 | protected string $method = Request::GET; |
||
28 | protected string $requestTarget = ''; |
||
29 | |||
30 | /** |
||
31 | * ClientRequest constructor. |
||
32 | * |
||
33 | * If body is provided, the content internally is encoded in JSON |
||
34 | * and stored in body Stream object. |
||
35 | * |
||
36 | * @param string $method |
||
37 | * @param UriInterface|string $uri |
||
38 | * @param mixed $body [optional] \Psr\Http\Message\StreamInterface|iterable|resource|callable|string|null |
||
39 | * @param array $headers [optional] |
||
40 | */ |
||
41 | public function __construct( |
||
42 | string $method, |
||
43 | string|UriInterface $uri, |
||
44 | string|iterable $body = null, |
||
45 | array $headers = []) |
||
46 | { |
||
47 | $this->uri = $uri instanceof UriInterface ? $uri : new Uri($uri); |
||
48 | $this->stream = create_stream($this->prepareBody($body)); |
||
49 | $this->setHost(); |
||
50 | $this->setMethod($method, $this); |
||
51 | $this->setHeaders($headers); |
||
52 | } |
||
53 | |||
54 | public function getMethod(): string |
||
55 | { |
||
56 | return strtoupper($this->method); |
||
57 | } |
||
58 | |||
59 | public function withMethod($method): ClientRequest |
||
60 | { |
||
61 | return $this->setMethod($method, clone $this); |
||
62 | } |
||
63 | |||
64 | public function getUri(): UriInterface |
||
65 | { |
||
66 | return $this->uri; |
||
67 | } |
||
68 | |||
69 | public function withUri(UriInterface $uri, $preserveHost = false): static |
||
70 | { |
||
71 | $instance = clone $this; |
||
72 | $instance->uri = $uri; |
||
73 | if (true === $preserveHost) { |
||
74 | return $instance->withHeader('Host', $this->uri->getHost() ?: $uri->getHost()); |
||
75 | } |
||
76 | return $instance->withHeader('Host', $uri->getHost()); |
||
77 | } |
||
78 | |||
79 | public function getRequestTarget(): string |
||
80 | { |
||
81 | if ($this->requestTarget) { |
||
82 | return $this->requestTarget; |
||
83 | } |
||
84 | $path = $this->uri->getPath(); |
||
85 | if (!$path && !$this->requestTarget) { |
||
86 | return '/'; |
||
87 | } |
||
88 | if ($query = $this->uri->getQuery()) { |
||
89 | $path .= '?' . $query; |
||
90 | } |
||
91 | return $path; |
||
92 | } |
||
93 | |||
94 | public function withRequestTarget($requestTarget): static |
||
95 | { |
||
96 | if (\preg_match('/\s+/', $requestTarget)) { |
||
97 | throw new \InvalidArgumentException( |
||
98 | self::E_INVALID_REQUEST_TARGET, |
||
99 | HttpStatus::BAD_REQUEST); |
||
100 | } |
||
101 | $instance = clone $this; |
||
102 | $instance->requestTarget = $requestTarget; |
||
103 | return $instance; |
||
104 | } |
||
105 | |||
106 | public function getPath(): string |
||
107 | { |
||
108 | return str_replace($_SERVER['SCRIPT_NAME'], '', $this->uri->getPath()) ?: '/'; |
||
109 | } |
||
110 | |||
111 | public function getBaseUri(): string |
||
112 | { |
||
113 | if (false === empty($host = $this->getUri()->getHost())) { |
||
114 | $port = $this->getUri()->getPort(); |
||
115 | $port && $port = ":{$port}"; |
||
0 ignored issues
–
show
|
|||
116 | return $this->getUri()->getScheme() . "://{$host}{$port}"; |
||
117 | } |
||
118 | return ''; |
||
119 | } |
||
120 | |||
121 | public function isSecure(): bool |
||
122 | { |
||
123 | return 'https' === $this->uri->getScheme(); |
||
124 | } |
||
125 | |||
126 | public function isSafeMethod(): bool |
||
127 | { |
||
128 | return \in_array($this->method, Request::SAFE_METHODS); |
||
129 | } |
||
130 | |||
131 | protected function setHost(): void |
||
132 | { |
||
133 | $this->headersMap['host'] = 'Host'; |
||
134 | |||
135 | $this->headers = ['Host' => $this->uri->getHost() ?: $_SERVER['HTTP_HOST'] ?? ''] + $this->headers; |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * @param string $method The HTTP method |
||
140 | * @param RequestInterface $instance |
||
141 | * |
||
142 | * @return static |
||
143 | */ |
||
144 | protected function setMethod(string $method, RequestInterface $instance): RequestInterface |
||
145 | { |
||
146 | $instance->method = \strtoupper($method); |
||
0 ignored issues
–
show
|
|||
147 | return $instance; |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * Checks if body is non-empty if HTTP method is one of the *safe* methods. |
||
152 | * The consuming code may disallow this and return the response object. |
||
153 | * |
||
154 | * @return Response|null |
||
155 | */ |
||
156 | protected function assertSafeMethod(): ?Response |
||
157 | { |
||
158 | if ($this->isSafeMethod() && $this->getBody()->getSize() > 0) { |
||
159 | return $this->getPhpError(HttpStatus::BAD_REQUEST, self::E_SAFE_METHODS_WITH_BODY); |
||
160 | } |
||
161 | return null; |
||
162 | } |
||
163 | |||
164 | /** |
||
165 | * @param mixed $body |
||
166 | * |
||
167 | * @return mixed If $body is iterable returns JSON stringified body, or whatever it is |
||
168 | */ |
||
169 | protected function prepareBody(mixed $body): mixed |
||
170 | { |
||
171 | if (\is_iterable($body)) { |
||
172 | return json_serialize($body); |
||
173 | } |
||
174 | return $body; |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * @param int $status |
||
179 | * @param string|null $message |
||
180 | * |
||
181 | * @return Response JSON error message |
||
182 | * @link https://tools.ietf.org/html/rfc7807 |
||
183 | */ |
||
184 | protected function getPhpError(int $status, ?string $message = null): Response |
||
185 | { |
||
186 | return new ServerResponse(json_serialize([ |
||
187 | 'title' => StatusCode::CODE[$status], |
||
188 | 'detail' => $message ?? \error_get_last()['message'], |
||
189 | 'instance' => (string)$this->getUri(), |
||
190 | 'type' => 'https://httpstatuses.com/' . (string)$status, |
||
191 | 'status' => $status, |
||
192 | ]), $status, ['Content-type' => 'application/problem+json']); |
||
193 | } |
||
194 | |||
195 | } |
||
196 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: