1<?php
2
3declare(strict_types=1);
4
5namespace GuzzleHttp\Psr7;
6
7use InvalidArgumentException;
8use Psr\Http\Message\RequestInterface;
9use Psr\Http\Message\StreamInterface;
10use Psr\Http\Message\UriInterface;
11
12/**
13 * PSR-7 request implementation.
14 */
15class Request implements RequestInterface
16{
17    use MessageTrait;
18
19    /** @var string */
20    private $method;
21
22    /** @var string|null */
23    private $requestTarget;
24
25    /** @var UriInterface */
26    private $uri;
27
28    /**
29     * @param string                               $method  HTTP method
30     * @param string|UriInterface                  $uri     URI
31     * @param (string|string[])[]                  $headers Request headers
32     * @param string|resource|StreamInterface|null $body    Request body
33     * @param string                               $version Protocol version
34     */
35    public function __construct(
36        string $method,
37        $uri,
38        array $headers = [],
39        $body = null,
40        string $version = '1.1'
41    ) {
42        $this->assertMethod($method);
43        if (!($uri instanceof UriInterface)) {
44            $uri = new Uri($uri);
45        }
46
47        $this->method = strtoupper($method);
48        $this->uri = $uri;
49        $this->setHeaders($headers);
50        $this->protocol = $version;
51
52        if (!isset($this->headerNames['host'])) {
53            $this->updateHostFromUri();
54        }
55
56        if ($body !== '' && $body !== null) {
57            $this->stream = Utils::streamFor($body);
58        }
59    }
60
61    public function getRequestTarget(): string
62    {
63        if ($this->requestTarget !== null) {
64            return $this->requestTarget;
65        }
66
67        $target = $this->uri->getPath();
68        if ($target === '') {
69            $target = '/';
70        }
71        if ($this->uri->getQuery() != '') {
72            $target .= '?'.$this->uri->getQuery();
73        }
74
75        return $target;
76    }
77
78    public function withRequestTarget($requestTarget): RequestInterface
79    {
80        if (preg_match('#\s#', $requestTarget)) {
81            throw new InvalidArgumentException(
82                'Invalid request target provided; cannot contain whitespace'
83            );
84        }
85
86        $new = clone $this;
87        $new->requestTarget = $requestTarget;
88
89        return $new;
90    }
91
92    public function getMethod(): string
93    {
94        return $this->method;
95    }
96
97    public function withMethod($method): RequestInterface
98    {
99        $this->assertMethod($method);
100        $new = clone $this;
101        $new->method = strtoupper($method);
102
103        return $new;
104    }
105
106    public function getUri(): UriInterface
107    {
108        return $this->uri;
109    }
110
111    public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
112    {
113        if ($uri === $this->uri) {
114            return $this;
115        }
116
117        $new = clone $this;
118        $new->uri = $uri;
119
120        if (!$preserveHost || !isset($this->headerNames['host'])) {
121            $new->updateHostFromUri();
122        }
123
124        return $new;
125    }
126
127    private function updateHostFromUri(): void
128    {
129        $host = $this->uri->getHost();
130
131        if ($host == '') {
132            return;
133        }
134
135        if (($port = $this->uri->getPort()) !== null) {
136            $host .= ':'.$port;
137        }
138
139        if (isset($this->headerNames['host'])) {
140            $header = $this->headerNames['host'];
141        } else {
142            $header = 'Host';
143            $this->headerNames['host'] = 'Host';
144        }
145        // Ensure Host is the first header.
146        // See: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
147        $this->headers = [$header => [$host]] + $this->headers;
148    }
149
150    /**
151     * @param mixed $method
152     */
153    private function assertMethod($method): void
154    {
155        if (!is_string($method) || $method === '') {
156            throw new InvalidArgumentException('Method must be a non-empty string.');
157        }
158    }
159}
160