ClientRequest   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 173
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 7
Bugs 0 Features 2
Metric Value
eloc 59
dl 0
loc 173
ccs 63
cts 63
cp 1
rs 10
c 7
b 0
f 2
wmc 30

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getMethod() 0 3 1
A withMethod() 0 3 1
A withUri() 0 8 3
A getUri() 0 3 1
A __construct() 0 11 2
A getBaseUri() 0 8 3
A getRequestTarget() 0 12 4
A withRequestTarget() 0 10 2
A setHost() 0 5 2
A prepareBody() 0 6 2
A getPhpError() 0 9 1
A setMethod() 0 4 1
A assertSafeMethod() 0 6 3
A isSecure() 0 3 1
A isSafeMethod() 0 3 1
A getPath() 0 3 2
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
        if (!$path = $this->uri->getPath()) {
85 15
            return '/';
86
        }
87
        if ($query = $this->uri->getQuery()) {
88 4
            $path .= '?' . $query;
89
        }
90 4
        return $path;
91 2
    }
92
93
    public function withRequestTarget($requestTarget): static
94 3
    {
95
        if (\preg_match('/\s+/', $requestTarget)) {
96 3
            throw new \InvalidArgumentException(
97 2
                self::E_INVALID_REQUEST_TARGET,
98
                HttpStatus::BAD_REQUEST);
99
        }
100 1
        $instance                = clone $this;
101 1
        $instance->requestTarget = $requestTarget;
102
        return $instance;
103
    }
104 1
105
    public function getPath(): string
106
    {
107 3
        return \str_replace($_SERVER['SCRIPT_NAME'], '', $this->uri->getPath()) ?: '/';
108
    }
109 3
110 1
    public function getBaseUri(): string
111
    {
112
        if (false === empty($host = $this->getUri()->getHost())) {
113 2
            $port = $this->getUri()->getPort();
114 2
            $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...
115
            return $this->getUri()->getScheme() . "://{$host}{$port}";
116 2
        }
117
        return '';
118
    }
119 2
120
    public function isSecure(): bool
121 2
    {
122
        return 'https' === $this->uri->getScheme();
123
    }
124 2
125
    public function isSafeMethod(): bool
126 2
    {
127 1
        return \in_array($this->method, Request::SAFE_METHODS);
128 1
    }
129
130 1
    protected function setHost(): void
131
    {
132
        $this->headersMap['host'] = 'Host';
133 1
134
        $this->headers = ['Host' => $this->uri->getHost() ?: $_SERVER['HTTP_HOST'] ?? ''] + $this->headers;
135
    }
136 1
137
    /**
138 1
     * @param string           $method The HTTP method
139
     * @param RequestInterface $instance
140
     *
141 76
     * @return static
142
     */
143 76
    protected function setMethod(string $method, RequestInterface $instance): RequestInterface
144
    {
145
        $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...
146 129
        return $instance;
147
    }
148 129
149
    /**
150 129
     * Checks if body is non-empty if HTTP method is one of the *safe* methods.
151 129
     * The consuming code may disallow this and return the response object.
152
     *
153
     * @return Response|null
154
     */
155
    protected function assertSafeMethod(): ?Response
156
    {
157
        if ($this->isSafeMethod() && $this->getBody()->getSize() > 0) {
158
            return $this->getPhpError(HttpStatus::BAD_REQUEST, self::E_SAFE_METHODS_WITH_BODY);
159 129
        }
160
        return null;
161 129
    }
162
163 129
    /**
164
     * @param mixed $body
165
     *
166
     * @return mixed If $body is iterable returns JSON stringified body, or whatever it is
167
     */
168
    protected function prepareBody(mixed $body): mixed
169
    {
170
        if (\is_iterable($body)) {
171
            return json_serialize($body);
172 28
        }
173
        return $body;
174 28
    }
175 2
176
    /**
177
     * @param int         $status
178 26
     * @param string|null $message
179
     *
180
     * @return Response JSON error message
181
     * @link https://tools.ietf.org/html/rfc7807
182
     */
183
    protected function getPhpError(int $status, ?string $message = null): Response
184
    {
185
        return new ServerResponse(json_serialize([
186 129
            'title'    => HttpStatus::CODE[$status],
187
            'detail'   => $message ?? \error_get_last()['message'] ?? HttpStatus::CODE[$status],
188 129
            'instance' => (string)$this->getUri(),
189 126
            'type'     => 'https://httpstatuses.com/' . $status,
190
            'status'   => $status,
191
        ]), $status, ['Content-Type' => 'application/problem+json']);
192 5
    }
193
}
194