Passed
Pull Request — master (#17)
by Mihail
15:10
created

ClientRequest::setMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 3
b 0
f 1
nc 1
nop 2
dl 0
loc 4
ccs 2
cts 2
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\{HttpMethod, HttpStatus, Request, Response};
16
use InvalidArgumentException;
17
use JsonSerializable;
18
use Psr\Http\Message\{RequestInterface, UriInterface};
19
use function error_get_last;
20
use function in_array;
21
use function is_iterable;
22
use function Koded\Stdlib\json_serialize;
23
use function preg_match;
24
use function str_replace;
25
use function strtoupper;
26
27
class ClientRequest implements RequestInterface, JsonSerializable
28
{
29
    use HeaderTrait, MessageTrait, JsonSerializeTrait;
30
31
    const E_INVALID_REQUEST_TARGET = 'The request target is invalid, it contains whitespaces';
32
    const E_SAFE_METHODS_WITH_BODY = 'failed to open stream: you should not set the message body with safe HTTP methods';
33
34
    protected UriInterface $uri;
35
    protected HttpMethod|string $method;
36
    protected string $requestTarget = '';
37
38
    /**
39
     * ClientRequest constructor.
40
     *
41
     * If body is provided, the content internally is encoded in JSON
42
     * and stored in body Stream object.
43
     *
44
     * @param HttpMethod          $method
45 129
     * @param UriInterface|string $uri
46
     * @param mixed               $body    [optional] \Psr\Http\Message\StreamInterface|iterable|resource|callable|string|null
47 129
     * @param array               $headers [optional]
48 129
     */
49
    public function __construct(
50 129
        HttpMethod $method,
51 129
        string|UriInterface $uri,
52 129
        string|iterable|null $body = null,
53 129
        array $headers = [])
54
    {
55 36
        $this->uri    = $uri instanceof UriInterface ? $uri : new Uri($uri);
56
        $this->stream = create_stream($this->prepareBody($body));
57 36
        $this->method = $method;
58
        $this->setHost();
59
        $this->setHeaders($headers);
60 11
    }
61
62 11
    public function getMethod(): string
63
    {
64
        return $this->method?->value ?? $this->method;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->method->value ?? $this->method could return the type Koded\Http\Interfaces\HttpMethod which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
65 30
    }
66
67 30
    public function withMethod(string $method): ClientRequest
68
    {
69
        $instance = clone $this;
70 16
        $instance->method = HttpMethod::tryFrom(strtoupper($method)) ?? $method;
71
        return $instance;
72 16
    }
73
74 16
    public function getUri(): UriInterface
75 3
    {
76 3
        return $this->uri;
77
    }
78 16
79
    public function withUri(UriInterface $uri, bool $preserveHost = false): static
80
    {
81 16
        $instance = clone $this;
82 2
        $instance->uri = $uri;
83
        if (true === $preserveHost) {
84
            return $instance->withHeader('Host', $this->uri->getHost() ?: $uri->getHost());
85 15
        }
86
        //return $instance->withHeader('Host', $uri->getHost());
87
        return $instance->withHeader('Host', $uri->getHost() ?: $this->uri->getHost());
88 4
    }
89
90 4
    public function getRequestTarget(): string
91 2
    {
92
        if ($this->requestTarget) {
93
            return $this->requestTarget;
94 3
        }
95
        if (!$path = $this->uri->getPath()) {
96 3
            return '/';
97 2
        }
98
        if ($query = $this->uri->getQuery()) {
99
            $path .= '?' . $query;
100 1
        }
101 1
        return $path;
102
    }
103
104 1
    public function withRequestTarget(string $requestTarget): static
105
    {
106
        if (preg_match('/\s+/', $requestTarget)) {
107 3
            throw new InvalidArgumentException(
108
                self::E_INVALID_REQUEST_TARGET,
109 3
                HttpStatus::BAD_REQUEST);
110 1
        }
111
        $instance                = clone $this;
112
        $instance->requestTarget = $requestTarget;
113 2
        return $instance;
114 2
    }
115
116 2
    public function getPath(): string
117
    {
118
        return str_replace($_SERVER['SCRIPT_NAME'], '', $this->uri->getPath()) ?: '/';
119 2
    }
120
121 2
    public function getBaseUri(): string
122
    {
123
        if (false === empty($host = $this->getUri()->getHost())) {
124 2
            $port = $this->getUri()->getPort();
125
            $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...
126 2
            return $this->getUri()->getScheme() . "://{$host}{$port}";
127 1
        }
128 1
        return '';
129
    }
130 1
131
    public function isSecure(): bool
132
    {
133 1
        return 'https' === $this->uri->getScheme();
134
    }
135
136 1
    public function isSafeMethod(): bool
137
    {
138 1
        return in_array($this->method, Request::SAFE_METHODS);
139
    }
140
141 76
    protected function setHost(): void
142
    {
143 76
        $this->headersMap['host'] = 'Host';
144
145
        $this->headers = ['Host' => $this->uri->getHost() ?: $_SERVER['HTTP_HOST'] ?? ''] + $this->headers;
146 129
    }
147
148 129
    /**
149
     * Checks if body is non-empty if HTTP method is one of the *safe* methods.
150 129
     * The consuming code may disallow this and return the response object.
151 129
     *
152
     * @return Response|null
153
     */
154
    protected function assertSafeMethod(): ?Response
155
    {
156
        if ($this->isSafeMethod() && $this->getBody()->getSize() > 0) {
157
            return $this->getPhpError(HttpStatus::BAD_REQUEST, self::E_SAFE_METHODS_WITH_BODY);
158
        }
159 129
        return null;
160
    }
161 129
162
    /**
163 129
     * @param mixed $body
164
     *
165
     * @return mixed If $body is iterable returns JSON stringified body, or whatever it is
166
     */
167
    protected function prepareBody(mixed $body): mixed
168
    {
169
        if (is_iterable($body)) {
170
            return json_serialize($body);
171
        }
172 28
        return $body;
173
    }
174 28
175 2
    /**
176
     * @param int         $status
177
     * @param string|null $message
178 26
     *
179
     * @return Response JSON error message
180
     * @link https://datatracker.ietf.org/doc/html/rfc9457
181
     */
182
    protected function getPhpError(int $status, ?string $message = null): Response
183
    {
184
        return new ServerResponse(json_serialize([
185
            'title'    => HttpStatus::CODE[$status],
186 129
            'detail'   => $message ?? error_get_last()['message'] ?? HttpStatus::CODE[$status],
187
            'instance' => (string)$this->getUri(),
188 129
            //'type'     => 'https://httpstatuses.com/' . $status,
189 126
            'status'   => $status,
190
        ]), $status, ['Content-Type' => 'application/problem+json']);
191
    }
192
}
193