Passed
Pull Request — master (#16)
by Mihail
14:44 queued 04:45
created

PhpClient::hasError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 3
cts 3
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\Client;
14
15
use Koded\Http\{ClientRequest, ServerResponse};
16
use Koded\Http\Interfaces\{HttpRequestClient, HttpStatus, Response};
17
use Psr\Http\Message\UriInterface;
18
use function Koded\Http\create_stream;
19
20
/**
21
 * @link http://php.net/manual/en/context.http.php
22
 */
23
class PhpClient extends ClientRequest implements HttpRequestClient
24
{
25
    use EncodingTrait, Psr18ClientTrait;
26
27
    /** @var array Stream context options */
28
    private array $options = [
29
        'protocol_version' => 1.1,
30
        'user_agent'       => self::USER_AGENT,
31
        'method'           => 'GET',
32
        'max_redirects'    => 20,
33
        'follow_location'  => 1,
34
        'ignore_errors'    => true,
35
        'request_fulluri'  => true,
36
        'ssl'              => [
37
            'verify_peer'       => true,
38
            'allow_self_signed' => false,
39
        ]
40
    ];
41
42 9
    public function __construct(
43
        string $method,
44 9
        string|UriInterface $uri,
45 9
        string|iterable $body = null,
46 9
        array $headers = [])
47
    {
48 13
        parent::__construct($method, $uri, $body, $headers);
49
        $this->options['timeout'] = (\ini_get('default_socket_timeout') ?: 10.0) * 1.0;
50 13
    }
51 1
52
    public function read(): Response
53
    {
54 12
        if ($resource = $this->assertSafeMethod()) {
55 12
            return $resource;
56
        }
57
        $this->prepareRequestBody();
58 12
        $this->prepareOptions();
59 4
        try {
60
            $resource = $this->createResource(\stream_context_create(['http' => $this->options]));
61
            if ($this->hasError($resource)) {
62 7
                return $this->getPhpError(HttpStatus::FAILED_DEPENDENCY);
63
            }
64 7
            $this->extractFromResponseHeaders($resource, $headers, $statusCode);
65 7
            return new ServerResponse(\stream_get_contents($resource), $statusCode, $headers);
66
        } catch (\ValueError $e) {
67
            return $this->getPhpError(HttpStatus::FAILED_DEPENDENCY, $e->getMessage());
68
        } catch (\Throwable $e) {
69 1
            return $this->getPhpError(
70 1
                $e->getCode() ?: HttpStatus::INTERNAL_SERVER_ERROR,
71
                $e->getMessage());
72 12
        } finally {
73 12
            if (\is_resource($resource)) {
74
                \fclose($resource);
75
            }
76
        }
77
    }
78 1
79
    public function userAgent(string $value): HttpRequestClient
80 1
    {
81
        $this->options['user_agent'] = $value;
82 1
        return $this;
83
    }
84
85 1
    public function followLocation(bool $value): HttpRequestClient
86
    {
87 1
        $this->options['follow_location'] = (int)$value;
88
        return $this;
89 1
    }
90
91
    public function maxRedirects(int $value): HttpRequestClient
92 1
    {
93
        $this->options['max_redirects'] = $value;
94 1
        return $this;
95
    }
96 1
97
    public function timeout(float $value): HttpRequestClient
98
    {
99 8
        $this->options['timeout'] = $value * 1.0;
100
        return $this;
101 8
    }
102
103 8
    public function ignoreErrors(bool $value): HttpRequestClient
104
    {
105
        $this->options['ignore_errors'] = $value;
106 1
        return $this;
107
    }
108 1
109
    public function verifySslHost(bool $value): HttpRequestClient
110 1
    {
111
        $this->options['ssl']['allow_self_signed'] = $value;
112
        return $this;
113 1
    }
114
115 1
    public function verifySslPeer(bool $value): HttpRequestClient
116
    {
117 1
        $this->options['ssl']['verify_peer'] = $value;
118
        return $this;
119
    }
120 1
121
    /**
122 1
     * @param resource $context from stream_context_create()
123
     *
124 1
     * @return resource|bool
125
     */
126
    protected function createResource($context)
127
    {
128
        return \fopen((string)$this->getUri(), 'rb', false, $context);
129
    }
130
131
    protected function prepareRequestBody(): void
132 10
    {
133
        if (!$this->stream->getSize()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->stream->getSize() of type integer|null is loosely compared to false; 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...
134 10
            return;
135
        }
136
        $this->stream->rewind();
137 12
        if (0 === $this->encoding) {
138
            $this->options['content'] = $this->stream->getContents();
139 12
        } elseif ($content = \json_decode($this->stream->getContents() ?: '[]', true)) {
140 9
            $this->normalizeHeader('Content-type', self::X_WWW_FORM_URLENCODED, true);
141
            $this->options['content'] = \http_build_query($content, null, '&', $this->encoding);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type string expected by parameter $numeric_prefix of http_build_query(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
            $this->options['content'] = \http_build_query($content, /** @scrutinizer ignore-type */ null, '&', $this->encoding);
Loading history...
142
        }
143 3
        $this->stream = create_stream($this->options['content']);
144
    }
145 3
146 1
    protected function hasError($resource): bool
147 2
    {
148 2
        return false === \is_resource($resource);
149 2
    }
150
151
    protected function prepareOptions(): void
152 3
    {
153 3
        $this->options['method'] = $this->getMethod();
154
        $this->options['header'] = $this->getFlattenedHeaders();
155 12
        unset($this->options['header'][0]); // Host header is always present and first
156
    }
157 12
158 12
    /**
159 12
     * Extracts the headers and status code from the response.
160 12
     *
161
     * @param resource $response   The resource from fopen()
162
     * @param array    $headers    Parsed response headers
163
     * @param int      $statusCode Response status code
164
     */
165
    protected function extractFromResponseHeaders($response, &$headers, &$statusCode): void
166
    {
167
        try {
168
            $meta = \stream_get_meta_data($response)['wrapper_data'] ?? [];
169 7
            /* HTTP status may not always be the first header in the response headers,
170
             * for example, if the stream follows one or multiple redirects, the last
171
             * status line is what is expected here.
172 7
             */
173
            $statusCode = \array_filter($meta, function(string $header) {
174 7
                return \str_starts_with($header, 'HTTP/');
175 7
            });
176 7
            $statusCode = \array_pop($statusCode) ?: 'HTTP/1.1 200 OK';
177 7
            $statusCode = (int)(\explode(' ', $statusCode)[1] ?? HttpStatus::OK);
178
            foreach ($meta as $header) {
179 7
                [$k, $v] = \explode(':', $header, 2) + [1 => null];
180 7
                if (null === $v) {
181 7
                    continue;
182 7
                }
183
                $headers[$k] = $v;
184 7
            }
185
        } finally {
186 7
            unset($meta, $header, $k, $v);
187 7
        }
188
    }
189
}
190