Completed
Push — master ( d2aa89...bbfa5f )
by Mihail
34:12 queued 24:23
created

ClientRequest::getPhpError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 7
c 1
b 0
f 1
nc 1
nop 2
dl 0
loc 9
ccs 4
cts 4
cp 1
crap 1
rs 10
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 129
        array $headers = [])
46
    {
47 129
        $this->uri    = $uri instanceof UriInterface ? $uri : new Uri($uri);
48 129
        $this->stream = create_stream($this->prepareBody($body));
49
        $this->setHost();
50 129
        $this->setMethod($method, $this);
51 129
        $this->setHeaders($headers);
52 129
    }
53 129
54
    public function getMethod(): string
55 36
    {
56
        return \strtoupper($this->method);
57 36
    }
58
59
    public function withMethod($method): ClientRequest
60 11
    {
61
        return $this->setMethod($method, clone $this);
62 11
    }
63
64
    public function getUri(): UriInterface
65 30
    {
66
        return $this->uri;
67 30
    }
68
69
    public function withUri(UriInterface $uri, $preserveHost = false): static
70 16
    {
71
        $instance = clone $this;
72 16
        $instance->uri = $uri;
73
        if (true === $preserveHost) {
74 16
            return $instance->withHeader('Host', $this->uri->getHost() ?: $uri->getHost());
75 3
        }
76 3
        return $instance->withHeader('Host', $uri->getHost());
77
    }
78 16
79
    public function getRequestTarget(): string
80
    {
81 16
        if ($this->requestTarget) {
82 2
            return $this->requestTarget;
83
        }
84
        $path = $this->uri->getPath();
85 15
        if (!$path && !$this->requestTarget) {
86
            return '/';
87
        }
88 4
        if ($query = $this->uri->getQuery()) {
89
            $path .= '?' . $query;
90 4
        }
91 2
        return $path;
92
    }
93
94 3
    public function withRequestTarget($requestTarget): static
95
    {
96 3
        if (\preg_match('/\s+/', $requestTarget)) {
97 2
            throw new \InvalidArgumentException(
98
                self::E_INVALID_REQUEST_TARGET,
99
                HttpStatus::BAD_REQUEST);
100 1
        }
101 1
        $instance                = clone $this;
102
        $instance->requestTarget = $requestTarget;
103
        return $instance;
104 1
    }
105
106
    public function getPath(): string
107 3
    {
108
        return \str_replace($_SERVER['SCRIPT_NAME'], '', $this->uri->getPath()) ?: '/';
109 3
    }
110 1
111
    public function getBaseUri(): string
112
    {
113 2
        if (false === empty($host = $this->getUri()->getHost())) {
114 2
            $port = $this->getUri()->getPort();
115
            $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...
116 2
            return $this->getUri()->getScheme() . "://{$host}{$port}";
117
        }
118
        return '';
119 2
    }
120
121 2
    public function isSecure(): bool
122
    {
123
        return 'https' === $this->uri->getScheme();
124 2
    }
125
126 2
    public function isSafeMethod(): bool
127 1
    {
128 1
        return \in_array($this->method, Request::SAFE_METHODS);
129
    }
130 1
131
    protected function setHost(): void
132
    {
133 1
        $this->headersMap['host'] = 'Host';
134
135
        $this->headers = ['Host' => $this->uri->getHost() ?: $_SERVER['HTTP_HOST'] ?? ''] + $this->headers;
136 1
    }
137
138 1
    /**
139
     * @param string           $method The HTTP method
140
     * @param RequestInterface $instance
141 76
     *
142
     * @return static
143 76
     */
144
    protected function setMethod(string $method, RequestInterface $instance): RequestInterface
145
    {
146 129
        $instance->method = \strtoupper($method);
0 ignored issues
show
Bug introduced by
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...
147
        return $instance;
148 129
    }
149
150 129
    /**
151 129
     * 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 129
            return $this->getPhpError(HttpStatus::BAD_REQUEST, self::E_SAFE_METHODS_WITH_BODY);
160
        }
161 129
        return null;
162
    }
163 129
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 28
            return json_serialize($body);
173
        }
174 28
        return $body;
175 2
    }
176
177
    /**
178 26
     * @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 129
        return new ServerResponse(json_serialize([
187
            'title'    => StatusCode::CODE[$status],
188 129
            'detail'   => $message ?? \error_get_last()['message'],
189 126
            'instance' => (string)$this->getUri(),
190
            'type'     => 'https://httpstatuses.com/' . $status,
191
            'status'   => $status,
192 5
        ]), $status, ['Content-type' => 'application/problem+json']);
193
    }
194
}
195