Completed
Push — master ( 3e15a8...ec1159 )
by Mihail
09:16
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\Request;
18
use Psr\Http\Message\{RequestInterface, UriInterface};
19
use function Koded\Stdlib\json_serialize;
20
21
22
class ClientRequest implements RequestInterface, JsonSerializable
23
{
24
    use HeaderTrait, MessageTrait, JsonSerializeTrait;
25
26
    const E_INVALID_REQUEST_TARGET = 'The request target is invalid, it contains whitespaces';
27
    const E_SAFE_METHODS_WITH_BODY = 'failed to open stream: you should not set the message body with safe HTTP methods';
28
29
    /** @var UriInterface */
30
    protected $uri;
31
    protected $method        = Request::GET;
32
    protected $requestTarget = '';
33
34
    /**
35
     * ClientRequest constructor.
36
     *
37
     * If body is provided, the content internally is encoded in JSON
38
     * and stored in body Stream object.
39
     *
40
     * @param string                                                                   $method
41
     * @param UriInterface|string                                                      $uri
42
     * @param \Psr\Http\Message\StreamInterface|iterable|resource|callable|string|null $body    [optional]
43
     * @param array                                                                    $headers [optional]
44
     */
45
    public function __construct(string $method, $uri, $body = null, array $headers = [])
46
    {
47
        $this->uri    = $uri instanceof UriInterface ? $uri : new Uri($uri);
48
        $this->stream = create_stream($this->prepareBody($body));
49
50
        $this->setHost();
51
        $this->setMethod($method, $this);
52
        $this->setHeaders($headers);
53
    }
54
55
    public function getMethod(): string
56
    {
57
        return strtoupper($this->method);
58
    }
59
60
    public function withMethod($method): ClientRequest
61
    {
62
        return $this->setMethod($method, clone $this);
63
    }
64
65
    public function getUri(): UriInterface
66
    {
67
        return $this->uri;
68
    }
69
70
    public function withUri(UriInterface $uri, $preserveHost = false): ClientRequest
71
    {
72
        $instance = clone $this;
73
74
        if (true === $preserveHost) {
75
            $uri      = $uri->withHost($this->uri->getHost());
76
            $instance = $instance->withUri($uri);
77
        } else {
78
            $instance->uri = $uri;
79
        }
80
81
        if (empty($instance->getHeader('host')) && $host = $uri->getHost()) {
82
            return $instance->withHeader('Host', $host);
83
        }
84
85
        return $instance->withHeader('Host', [$uri->getHost()]);
86
    }
87
88
    public function getRequestTarget(): string
89
    {
90
        if ($this->requestTarget) {
91
            return $this->requestTarget;
92
        }
93
94
        $path = $this->uri->getPath();
95
96
        if (!$path && !$this->requestTarget) {
97
            return '/';
98
        }
99
100
        if ($query = $this->uri->getQuery()) {
101
            $path .= '?' . $query;
102
        }
103
104
        return $path;
105
    }
106
107
    public function withRequestTarget($requestTarget): ClientRequest
108
    {
109
        if (preg_match('/\s+/', $requestTarget)) {
110
            throw new InvalidArgumentException(self::E_INVALID_REQUEST_TARGET, StatusCode::BAD_REQUEST);
111
        }
112
113
        $instance                = clone $this;
114
        $instance->requestTarget = $requestTarget;
115
116
        return $instance;
117
    }
118
119
    public function getPath(): string
120
    {
121
        return str_replace($_SERVER['SCRIPT_NAME'], '', $this->uri->getPath()) ?: '/';
122
    }
123
124
    public function getBaseUri(): string
125
    {
126
        if (false === empty($host = $this->getUri()->getHost())) {
127
            $port = $this->getUri()->getPort();
128
            $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...
129
130
            return $this->getUri()->getScheme() . "://{$host}{$port}";
131
        }
132
133
        return '';
134
    }
135
136
    public function isSecure(): bool
137
    {
138
        return 'https' === $this->uri->getScheme();
139
    }
140
141
    public function isSafeMethod(): bool
142
    {
143
        return in_array($this->method, Request::SAFE_METHODS);
144
    }
145
146
    protected function setHost(): void
147
    {
148
        $this->headersMap['host'] = 'Host';
149
150
        $this->headers = ['Host' => $this->uri->getHost() ?: $_SERVER['HTTP_HOST'] ?? ''] + $this->headers;
151
    }
152
153
    /**
154
     * @param string           $method The HTTP method
155
     * @param RequestInterface $instance
156
     *
157
     * @return self
158
     */
159
    protected function setMethod(string $method, RequestInterface $instance): RequestInterface
160
    {
161
        $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...
162
163
        return $instance;
164
    }
165
166
    /**
167
     * Checks if body is non-empty if HTTP method is one of the *safe* methods.
168
     * The consuming code may disallow this and return the response object.
169
     *
170
     * @return ServerResponse|null
171
     */
172
    protected function assertSafeMethod(): ?ServerResponse
173
    {
174
        if ($this->isSafeMethod() && $this->getBody()->getSize() > 0) {
175
            return new ServerResponse(self::E_SAFE_METHODS_WITH_BODY, StatusCode::BAD_REQUEST);
176
        }
177
178
        return null;
179
    }
180
181
    /**
182
     * @param mixed $body
183
     *
184
     * @return mixed If $body is iterable returns JSON stringified body, or whatever it is
185
     */
186
    protected function prepareBody($body)
187
    {
188
        if (false === is_iterable($body)) {
189
            return $body;
190
        }
191
192
        return json_serialize($body);
193
    }
194
}
195