Completed
Push — v3 ( c94160...3fd6a7 )
by Mihail
06:23
created

ClientRequest.php (2 issues)

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 InvalidArgumentException;
16
use JsonSerializable;
17
use Koded\Http\Interfaces\{HttpStatus, Request};
18
use Psr\Http\Message\{RequestInterface, UriInterface};
19
use function Koded\Stdlib\json_serialize;
20
21
class ClientRequest implements RequestInterface, JsonSerializable
22
{
23
    use HeaderTrait, MessageTrait, JsonSerializeTrait;
24
25
    const E_INVALID_REQUEST_TARGET = 'The request target is invalid, it contains whitespaces';
26
    const E_SAFE_METHODS_WITH_BODY = 'failed to open stream: you should not set the message body with safe HTTP methods';
27
28
    protected UriInterface $uri;
29
    protected string $method        = Request::GET;
30
    protected string $requestTarget = '';
31
32
    /**
33
     * ClientRequest constructor.
34
     *
35
     * If body is provided, the content internally is encoded in JSON
36
     * and stored in body Stream object.
37
     *
38
     * @param string              $method
39
     * @param UriInterface|string $uri
40
     * @param mixed               $body    [optional] \Psr\Http\Message\StreamInterface|iterable|resource|callable|string|null
41
     * @param array               $headers [optional]
42
     */
43
    public function __construct(
44
        string $method,
45
        /*UriInterface|string*/ $uri,
46
        /*iterable|string*/ $body = null,
47
        array $headers = [])
48
    {
49
        $this->uri    = $uri instanceof UriInterface ? $uri : new Uri($uri);
50
        $this->stream = create_stream($this->prepareBody($body));
51
        $this->setHost();
52
        $this->setMethod($method, $this);
53
        $this->setHeaders($headers);
54
    }
55
56
    public function getMethod(): string
57
    {
58
        return strtoupper($this->method);
59
    }
60
61
    public function withMethod($method): ClientRequest
62
    {
63
        return $this->setMethod($method, clone $this);
64
    }
65
66
    public function getUri(): UriInterface
67
    {
68
        return $this->uri;
69
    }
70
71
    public function withUri(UriInterface $uri, $preserveHost = false): ClientRequest
72
    {
73
        $instance = clone $this;
74
        if (true === $preserveHost) {
75
            $uri      = $uri->withHost($this->uri->getHost());
76
            $instance = $instance->withUri($uri);
77
        } else {
78
            $instance->uri = $uri;
79
        }
80
        if (empty($instance->getHeader('host')) && $host = $uri->getHost()) {
81
            return $instance->withHeader('Host', $host);
82
        }
83
        return $instance->withHeader('Host', [$uri->getHost()]);
84
    }
85
86
    public function getRequestTarget(): string
87
    {
88
        if ($this->requestTarget) {
89
            return $this->requestTarget;
90
        }
91
        $path = $this->uri->getPath();
92
        if (!$path && !$this->requestTarget) {
93
            return '/';
94
        }
95
        if ($query = $this->uri->getQuery()) {
96
            $path .= '?' . $query;
97
        }
98
        return $path;
99
    }
100
101
    public function withRequestTarget($requestTarget): ClientRequest
102
    {
103
        if (preg_match('/\s+/', $requestTarget)) {
104
            throw new InvalidArgumentException(
105
                self::E_INVALID_REQUEST_TARGET,
106
                HttpStatus::BAD_REQUEST);
107
        }
108
        $instance                = clone $this;
109
        $instance->requestTarget = $requestTarget;
110
        return $instance;
111
    }
112
113
    public function getPath(): string
114
    {
115
        return str_replace($_SERVER['SCRIPT_NAME'], '', $this->uri->getPath()) ?: '/';
116
    }
117
118
    public function getBaseUri(): string
119
    {
120
        if (false === empty($host = $this->getUri()->getHost())) {
121
            $port = $this->getUri()->getPort();
122
            $port && $port = ":{$port}";
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
123
            return $this->getUri()->getScheme() . "://{$host}{$port}";
124
        }
125
        return '';
126
    }
127
128
    public function isSecure(): bool
129
    {
130
        return 'https' === $this->uri->getScheme();
131
    }
132
133
    public function isSafeMethod(): bool
134
    {
135
        return in_array($this->method, Request::SAFE_METHODS);
136
    }
137
138
    protected function setHost(): void
139
    {
140
        $this->headersMap['host'] = 'Host';
141
142
        $this->headers = ['Host' => $this->uri->getHost() ?: $_SERVER['HTTP_HOST'] ?? ''] + $this->headers;
143
    }
144
145
    /**
146
     * @param string           $method The HTTP method
147
     * @param RequestInterface $instance
148
     *
149
     * @return self
150
     */
151
    protected function setMethod(string $method, RequestInterface $instance): RequestInterface
152
    {
153
        $instance->method = strtoupper($method);
0 ignored issues
show
Accessing method on the interface Psr\Http\Message\RequestInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
154
        return $instance;
155
    }
156
157
    /**
158
     * Checks if body is non-empty if HTTP method is one of the *safe* methods.
159
     * The consuming code may disallow this and return the response object.
160
     *
161
     * @return ServerResponse|null
162
     */
163
    protected function assertSafeMethod(): ?ServerResponse
164
    {
165
        if ($this->isSafeMethod() && $this->getBody()->getSize() > 0) {
166
            return new ServerResponse(self::E_SAFE_METHODS_WITH_BODY, HttpStatus::BAD_REQUEST);
167
        }
168
        return null;
169
    }
170
171
    /**
172
     * @param mixed $body
173
     *
174
     * @return mixed If $body is iterable returns JSON stringified body, or whatever it is
175
     */
176
    protected function prepareBody($body)
177
    {
178
        if (is_iterable($body)) {
179
            return json_serialize($body);
180
        }
181
        return $body;
182
    }
183
}
184