Skip to content

Commit 38cd524

Browse files
committed
Add Request and RequestTrait
1 parent af01f41 commit 38cd524

File tree

5 files changed

+507
-0
lines changed

5 files changed

+507
-0
lines changed

src/.gitkeep

Whitespace-only changes.

src/Request.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HttpSoft\Request;
6+
7+
use Fig\Http\Message\RequestMethodInterface;
8+
use HttpSoft\Uri\UriData;
9+
use Psr\Http\Message\RequestInterface;
10+
use Psr\Http\Message\StreamInterface;
11+
use Psr\Http\Message\UriInterface;
12+
13+
class Request implements RequestInterface, RequestMethodInterface
14+
{
15+
use RequestTrait;
16+
17+
/**
18+
* @param string $method
19+
* @param UriInterface|string $uri
20+
* @param StreamInterface|string|resource $body
21+
* @param array $headers
22+
* @param string $protocol
23+
*/
24+
public function __construct(
25+
string $method = self::METHOD_GET,
26+
$uri = UriData::EMPTY_STRING,
27+
$body = 'php://temp',
28+
array $headers = [],
29+
string $protocol = '1.1'
30+
) {
31+
$this->init($method, $uri, $body, $headers, $protocol);
32+
}
33+
}

src/RequestTrait.php

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace HttpSoft\Request;
6+
7+
use Fig\Http\Message\RequestMethodInterface;
8+
use HttpSoft\Stream\MessageTrait;
9+
use HttpSoft\Stream\StreamFactory;
10+
use HttpSoft\Uri\UriData;
11+
use HttpSoft\Uri\UriFactory;
12+
use InvalidArgumentException;
13+
use Psr\Http\Message\RequestInterface;
14+
use Psr\Http\Message\StreamInterface;
15+
use Psr\Http\Message\UriInterface;
16+
17+
use function gettype;
18+
use function get_class;
19+
use function in_array;
20+
use function is_object;
21+
use function is_string;
22+
use function preg_match;
23+
use function sprintf;
24+
use function strtoupper;
25+
26+
/**
27+
* Trait implementing the methods defined in `Psr\Http\Message\RequestInterface`.
28+
*
29+
* @see https://github.com/php-fig/http-message/tree/master/src/RequestInterface.php
30+
*/
31+
trait RequestTrait
32+
{
33+
use MessageTrait;
34+
35+
/**
36+
* @var string
37+
*/
38+
private string $method = RequestMethodInterface::METHOD_GET;
39+
40+
/**
41+
* @var null|string
42+
*/
43+
private ?string $requestTarget = null;
44+
45+
/**
46+
* @var UriInterface
47+
*/
48+
private UriInterface $uri;
49+
50+
/**
51+
* @var array
52+
*/
53+
private array $methods = [
54+
RequestMethodInterface::METHOD_HEAD,
55+
RequestMethodInterface::METHOD_GET,
56+
RequestMethodInterface::METHOD_POST,
57+
RequestMethodInterface::METHOD_PUT,
58+
RequestMethodInterface::METHOD_PATCH,
59+
RequestMethodInterface::METHOD_DELETE,
60+
RequestMethodInterface::METHOD_PURGE,
61+
RequestMethodInterface::METHOD_OPTIONS,
62+
RequestMethodInterface::METHOD_TRACE,
63+
RequestMethodInterface::METHOD_CONNECT,
64+
];
65+
66+
/**
67+
* Retrieves the message's request target.
68+
*
69+
* Retrieves the message's request-target either as it will appear (for
70+
* clients), as it appeared at request (for servers), or as it was
71+
* specified for the instance (see withRequestTarget()).
72+
*
73+
* In most cases, this will be the origin-form of the composed URI,
74+
* unless a value was provided to the concrete implementation (see
75+
* withRequestTarget() below).
76+
*
77+
* If no URI is available, and no request-target has been specifically
78+
* provided, this method MUST return the string "/".
79+
*
80+
* @return string
81+
*/
82+
public function getRequestTarget(): string
83+
{
84+
if ($this->requestTarget !== null) {
85+
return $this->requestTarget;
86+
}
87+
88+
$target = $this->uri->getPath();
89+
90+
if ($target && $query = $this->uri->getQuery()) {
91+
$target .= '?' . $query;
92+
}
93+
94+
return $target ?: '/';
95+
}
96+
97+
/**
98+
* Return an instance with the specific request-target.
99+
*
100+
* If the request needs a non-origin-form request-target — e.g., for
101+
* specifying an absolute-form, authority-form, or asterisk-form —
102+
* this method may be used to create an instance with the specified
103+
* request-target, verbatim.
104+
*
105+
* This method MUST be implemented in such a way as to retain the
106+
* immutability of the message, and MUST return an instance that has the
107+
* changed request target.
108+
*
109+
* @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
110+
* request-target forms allowed in request messages)
111+
* @param mixed $requestTarget
112+
* @return static
113+
*/
114+
public function withRequestTarget($requestTarget): RequestInterface
115+
{
116+
if ($requestTarget === $this->requestTarget) {
117+
return $this;
118+
}
119+
120+
if (is_string($requestTarget) && preg_match('/\s/', $requestTarget)) {
121+
throw new InvalidArgumentException(sprintf(
122+
'`%s` is not valid request target. Request target cannot contain whitespace.',
123+
$requestTarget
124+
));
125+
}
126+
127+
$new = clone $this;
128+
$new->requestTarget = $requestTarget;
129+
return $new;
130+
}
131+
132+
/**
133+
* Retrieves the HTTP method of the request.
134+
*
135+
* @return string Returns the request method.
136+
*/
137+
public function getMethod(): string
138+
{
139+
return $this->method;
140+
}
141+
142+
/**
143+
* Return an instance with the provided HTTP method.
144+
*
145+
* While HTTP method names are typically all uppercase characters, HTTP
146+
* method names are case-sensitive and thus implementations SHOULD NOT
147+
* modify the given string.
148+
*
149+
* This method MUST be implemented in such a way as to retain the
150+
* immutability of the message, and MUST return an instance that has the
151+
* changed request method.
152+
*
153+
* @param string $method Case-sensitive method.
154+
* @return static
155+
* @throws InvalidArgumentException for invalid HTTP methods.
156+
*/
157+
public function withMethod($method): RequestInterface
158+
{
159+
if ($method === $this->method) {
160+
return $this;
161+
}
162+
163+
if (!is_string($method)) {
164+
throw new InvalidArgumentException(sprintf(
165+
'Invalid HTTP method. Must be a string type, received `%s`',
166+
(is_object($method) ? get_class($method) : gettype($method))
167+
));
168+
}
169+
170+
$new = clone $this;
171+
$new->setMethod($method);
172+
return $new;
173+
}
174+
175+
/**
176+
* Retrieves the URI instance.
177+
*
178+
* This method MUST return a UriInterface instance.
179+
*
180+
* @link http://tools.ietf.org/html/rfc3986#section-4.3
181+
* @return UriInterface Returns a UriInterface instance
182+
* representing the URI of the request.
183+
*/
184+
public function getUri(): UriInterface
185+
{
186+
return $this->uri;
187+
}
188+
189+
/**
190+
* Returns an instance with the provided URI.
191+
*
192+
* This method MUST update the Host header of the returned request by
193+
* default if the URI contains a host component. If the URI does not
194+
* contain a host component, any pre-existing Host header MUST be carried
195+
* over to the returned request.
196+
*
197+
* You can opt-in to preserving the original state of the Host header by
198+
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
199+
* `true`, this method interacts with the Host header in the following ways:
200+
*
201+
* - If the Host header is missing or empty, and the new URI contains
202+
* a host component, this method MUST update the Host header in the returned
203+
* request.
204+
* - If the Host header is missing or empty, and the new URI does not contain a
205+
* host component, this method MUST NOT update the Host header in the returned
206+
* request.
207+
* - If a Host header is present and non-empty, this method MUST NOT update
208+
* the Host header in the returned request.
209+
*
210+
* This method MUST be implemented in such a way as to retain the
211+
* immutability of the message, and MUST return an instance that has the
212+
* new UriInterface instance.
213+
*
214+
* @link http://tools.ietf.org/html/rfc3986#section-4.3
215+
* @param UriInterface $uri New request URI to use.
216+
* @param bool $preserveHost Preserve the original state of the Host header.
217+
* @return static
218+
* @throws InvalidArgumentException for invalid URI.
219+
*/
220+
public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
221+
{
222+
if ($uri === $this->uri) {
223+
return $this;
224+
}
225+
226+
$new = clone $this;
227+
$new->setUri($uri);
228+
229+
if (!$preserveHost || !$this->hasHeader('host')) {
230+
$new->updateHostHeaderFromUri();
231+
}
232+
233+
return $new;
234+
}
235+
236+
/**
237+
* @param string $method
238+
* @param UriInterface|string $uri
239+
* @param StreamInterface|string|resource $body
240+
* @param array $headers
241+
* @param string $protocol
242+
*/
243+
private function init(
244+
string $method = RequestMethodInterface::METHOD_GET,
245+
$uri = UriData::EMPTY_STRING,
246+
$body = 'php://temp',
247+
array $headers = [],
248+
string $protocol = '1.1'
249+
): void {
250+
$this->setMethod($method);
251+
$this->setUri($uri);
252+
253+
$this->stream = StreamFactory::create($body);
254+
$this->registerHeaders($headers);
255+
$this->registerProtocolVersion($protocol);
256+
257+
if (!$this->hasHeader('host')) {
258+
$this->updateHostHeaderFromUri();
259+
}
260+
}
261+
262+
/**
263+
* Set and validate the HTTP method
264+
*
265+
* @param mixed $method
266+
* @throws InvalidArgumentException for invalid HTTP method.
267+
*/
268+
private function setMethod(string $method): void
269+
{
270+
$method = strtoupper($method);
271+
272+
if (!in_array($method, $this->methods) && !preg_match('/^[!#$%&\'*+.^_`|~0-9a-z-]+$/i', $method)) {
273+
throw new InvalidArgumentException(sprintf('`%s` is not valid HTTP method.', $method));
274+
}
275+
276+
$this->method = $method;
277+
}
278+
279+
/**
280+
* @param UriInterface|string $uri
281+
* @throws InvalidArgumentException for invalid URI.
282+
*/
283+
private function setUri($uri): void
284+
{
285+
if ($uri instanceof UriInterface) {
286+
$this->uri = $uri;
287+
return;
288+
}
289+
290+
if (is_string($uri)) {
291+
$this->uri = UriFactory::create($uri);
292+
return;
293+
}
294+
295+
throw new InvalidArgumentException(sprintf(
296+
'`%s` is not valid URI. Must be `null` or a `string` or a `Psr\Http\Message\UriInterface` instance.',
297+
(is_object($uri) ? get_class($uri) : gettype($uri))
298+
));
299+
}
300+
301+
/**
302+
* Updates `Host` header from the current URI and sets the `Host` first in the list of headers.
303+
*
304+
* @see https://tools.ietf.org/html/rfc7230#section-5.4
305+
*/
306+
private function updateHostHeaderFromUri(): void
307+
{
308+
if (!$host = $this->uri->getHost()) {
309+
return;
310+
}
311+
312+
if ($port = $this->uri->getPort()) {
313+
$host .= ':' . $port;
314+
}
315+
316+
$this->headerNames['host'] ??= 'Host';
317+
$this->headers = [$this->headerNames['host'] => [$host]] + $this->headers;
318+
}
319+
}

tests/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)