Skip to content

Commit 0e1bbf5

Browse files
committed
fix
1 parent dcc265b commit 0e1bbf5

File tree

5 files changed

+96
-109
lines changed

5 files changed

+96
-109
lines changed

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "codin/http-client",
3+
"version": "1.0.0",
34
"description": "Tiny PSR-18 Http Client",
45
"license": "MIT",
56
"type": "library",
@@ -17,7 +18,7 @@
1718
"psr/http-client-implementation": "1.0"
1819
},
1920
"require": {
20-
"php": ">=7.4",
21+
"php": ">=8.3",
2122
"ext-curl": "*",
2223
"nyholm/psr7": "@stable",
2324
"psr/http-client": "@stable",
@@ -50,7 +51,7 @@
5051
"phpstan analyse",
5152
"phpspec run"
5253
],
53-
"uninstall": [
54+
"clean": [
5455
"rm -rf ./bin",
5556
"rm -rf ./vendor",
5657
"rm ./composer.lock"

phpstan.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ parameters:
55
bootstrapFiles:
66
- %currentWorkingDirectory%/vendor/autoload.php
77
inferPrivatePropertyTypeFromConstructor: true
8-
checkMissingIterableValueType: false

src/HttpClient.php

Lines changed: 70 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,61 @@
44

55
namespace Codin\HttpClient;
66

7+
use CurlHandle;
78
use Psr\Http\Client\ClientInterface;
89
use Psr\Http\Message\RequestInterface;
910
use Psr\Http\Message\ResponseFactoryInterface;
1011
use Psr\Http\Message\ResponseInterface;
1112
use Psr\Http\Message\StreamFactoryInterface;
1213
use Psr\Http\Message\StreamInterface;
1314

14-
class HttpClient implements ClientInterface
15+
readonly class HttpClient implements ClientInterface
1516
{
1617
public const VERSION = '1.0';
1718

18-
protected ResponseFactoryInterface $responseFactory;
19-
20-
protected StreamFactoryInterface $streamFactory;
21-
22-
protected array $options;
23-
24-
protected bool $debug;
25-
26-
protected array $metrics = [];
27-
2819
/**
29-
* @var \CurlHandle
20+
* @param array<string, mixed> $options
3021
*/
31-
protected $session;
32-
3322
public function __construct(
34-
ResponseFactoryInterface $responseFactory,
35-
StreamFactoryInterface $streamFactory,
36-
array $options = [],
37-
bool $debug = false
23+
private ResponseFactoryInterface $responseFactory,
24+
private StreamFactoryInterface $streamFactory,
25+
private array $options = [],
3826
) {
39-
$this->responseFactory = $responseFactory;
40-
$this->streamFactory = $streamFactory;
41-
$this->options = $options;
42-
$this->debug = $debug;
43-
$this->session = curl_init();
4427
}
4528

46-
public function __destruct()
29+
private function parseHeaders(ResponseInterface $response, StreamInterface $headers): ResponseInterface
4730
{
48-
if (is_resource($this->session)) {
49-
curl_close($this->session);
31+
$data = rtrim((string) $headers);
32+
$parts = explode("\r\n\r\n", $data);
33+
$last = array_pop($parts);
34+
$lines = explode("\r\n", $last);
35+
$status = array_shift($lines);
36+
37+
if (is_string($status) && strpos($status, 'HTTP/') === 0) {
38+
[$version, $status, $message] = explode(' ', substr($status, strlen('http/')), 3);
39+
$response = $response->withProtocolVersion($version)->withStatus((int) $status, $message);
5040
}
41+
42+
return array_reduce($lines, static function (ResponseInterface $response, string $line): ResponseInterface {
43+
[$name, $value] = explode(':', $line, 2);
44+
return $response->withHeader($name, $value);
45+
}, $response);
5146
}
5247

53-
public function getMetrics(): array
48+
private function buildResponse(StreamInterface $headers, StreamInterface $body): ResponseInterface
5449
{
55-
return $this->metrics;
50+
if ($body->isSeekable()) {
51+
$body->rewind();
52+
}
53+
$response = $this->responseFactory->createResponse(200)->withBody($body);
54+
55+
return $this->parseHeaders($response, $headers);
5656
}
5757

58-
protected function buildOptions(RequestInterface $request): array
58+
/**
59+
* @return array<int, mixed>
60+
*/
61+
private function buildOptions(RequestInterface $request): array
5962
{
6063
$options = [
6164
CURLOPT_URL => (string) $request->getUri(),
@@ -71,16 +74,9 @@ protected function buildOptions(RequestInterface $request): array
7174
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
7275
CURLOPT_COOKIEFILE => '',
7376
CURLOPT_FOLLOWLOCATION => true,
77+
CURLOPT_CUSTOMREQUEST => $request->getMethod(),
7478
];
7579

76-
if ('POST' === $request->getMethod()) {
77-
$options[CURLOPT_POST] = true;
78-
} elseif ('HEAD' === $request->getMethod()) {
79-
$options[CURLOPT_NOBODY] = true;
80-
} else {
81-
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
82-
}
83-
8480
if ($request->getProtocolVersion() === '1.1') {
8581
$options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
8682
} elseif ($request->getProtocolVersion() === '2.0') {
@@ -102,15 +98,26 @@ protected function buildOptions(RequestInterface $request): array
10298
}
10399
}
104100

105-
if (in_array($request->getMethod(), ['PUT', 'POST', 'PATCH'])) {
106-
if ('POST' !== $request->getMethod()) {
107-
$options[CURLOPT_UPLOAD] = true;
101+
if ($request->getBody()->getSize() > 0) {
102+
$size = $request->hasHeader('Content-Length')
103+
? (int) $request->getHeaderLine('Content-Length')
104+
: null;
105+
106+
$options[CURLOPT_UPLOAD] = true;
107+
108+
// If the Expect header is not present, prevent curl from adding it
109+
if (!$request->hasHeader('Expect')) {
110+
$options[CURLOPT_HTTPHEADER][] = 'Expect:';
108111
}
109112

110-
if ($request->hasHeader('Content-Length')) {
111-
$options[CURLOPT_INFILESIZE] = $request->getHeader('Content-Length')[0];
112-
} elseif (!$request->hasHeader('Transfer-Encoding')) {
113-
$options[CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked';
113+
// cURL sometimes adds a content-type by default. Prevent this.
114+
if (!$request->hasHeader('Content-Type')) {
115+
$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
116+
}
117+
118+
if ($size !== null) {
119+
$options[CURLOPT_INFILESIZE] = $size;
120+
$request = $request->withoutHeader('Content-Length');
114121
}
115122

116123
if ($request->getBody()->isSeekable() && $request->getBody()->tell() > 0) {
@@ -122,46 +129,20 @@ protected function buildOptions(RequestInterface $request): array
122129
};
123130
}
124131

125-
return $this->options + $options;
126-
}
127-
128-
protected function parseHeaders(ResponseInterface $response, StreamInterface $headers): ResponseInterface
129-
{
130-
$data = rtrim((string) $headers);
131-
$parts = explode("\r\n\r\n", $data);
132-
$last = array_pop($parts);
133-
$lines = explode("\r\n", $last);
134-
$status = array_shift($lines);
135-
136-
if (is_string($status) && strpos($status, 'HTTP/') === 0) {
137-
[$version, $status, $message] = explode(' ', substr($status, strlen('http/')), 3);
138-
$response = $response->withProtocolVersion($version)->withStatus((int) $status, $message);
139-
}
140-
141-
return array_reduce($lines, static function (ResponseInterface $response, string $line): ResponseInterface {
142-
[$name, $value] = explode(':', $line, 2);
143-
return $response->withHeader($name, $value);
144-
}, $response);
145-
}
146-
147-
protected function buildResponse(StreamInterface $headers, StreamInterface $body): ResponseInterface
148-
{
149-
if ($body->isSeekable()) {
150-
$body->rewind();
151-
}
152-
$response = $this->responseFactory->createResponse(200)->withBody($body);
153-
154-
return $this->parseHeaders($response, $headers);
132+
return $options;
155133
}
156134

157-
protected function prepareSession(RequestInterface $request): array
135+
/**
136+
* @return array{0: StreamInterface, 1: StreamInterface}
137+
*/
138+
private function prepareSession(RequestInterface $request, CurlHandle $session): array
158139
{
159-
curl_setopt_array($this->session, $this->buildOptions($request));
140+
curl_setopt_array($session, $this->buildOptions($request));
160141

161142
$headers = $this->streamFactory->createStream('');
162143

163144
curl_setopt(
164-
$this->session,
145+
$session,
165146
CURLOPT_HEADERFUNCTION,
166147
static function ($session, string $data) use ($headers): int {
167148
return $headers->write($data);
@@ -171,7 +152,7 @@ static function ($session, string $data) use ($headers): int {
171152
$body = $this->streamFactory->createStream('');
172153

173154
curl_setopt(
174-
$this->session,
155+
$session,
175156
CURLOPT_WRITEFUNCTION,
176157
static function ($session, string $data) use ($body): int {
177158
return $body->write($data);
@@ -183,16 +164,21 @@ static function ($session, string $data) use ($body): int {
183164

184165
public function sendRequest(RequestInterface $request): ResponseInterface
185166
{
186-
[$headers, $body] = $this->prepareSession($request);
167+
$session = curl_init();
168+
169+
[$headers, $body] = $this->prepareSession($request, $session);
187170

188-
$result = curl_exec($this->session);
189-
if ($this->debug) {
190-
$this->metrics = curl_getinfo($this->session);
171+
$result = curl_exec($session);
172+
if (isset($this->options['metrics']) && is_callable($this->options['metrics'])) {
173+
$metrics = curl_getinfo($session);
174+
$this->options['metrics']($metrics);
191175
}
192-
curl_reset($this->session);
176+
$errorMessage = curl_error($session);
177+
$errorCode = curl_errno($session);
178+
curl_close($session);
193179

194180
if (false === $result) {
195-
throw new Exceptions\TransportError(curl_error($this->session), curl_errno($this->session), $request);
181+
throw new Exceptions\TransportError($errorMessage, $errorCode, $request);
196182
}
197183

198184
$response = $this->buildResponse($headers, $body);

src/MultipartBuilder.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@
1010

1111
class MultipartBuilder
1212
{
13-
protected StreamInterface $stream;
13+
private StreamInterface $stream;
1414

15-
protected string $boundary;
16-
17-
public function __construct(StreamFactoryInterface $streamFactory, ?string $boundary = null)
18-
{
15+
public function __construct(
16+
StreamFactoryInterface $streamFactory,
17+
private ?string $boundary = null
18+
) {
1919
$this->stream = $streamFactory->createStream('');
2020
$this->boundary = null === $boundary ? uniqid('', true) : $boundary;
2121
}
2222

23-
protected function write(string $data, string $newline = "\r\n"): void
23+
private function write(string $data, string $newline = "\r\n"): void
2424
{
2525
$this->stream->write($data . $newline);
2626
}
2727

28+
/**
29+
* @param array<string, string> $headers
30+
*/
2831
public function add(string $name, string $data, array $headers = []): void
2932
{
3033
$headers = array_change_key_case($headers);

src/RequestBuilder.php

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Psr\Http\Message\RequestInterface;
99
use Psr\Http\Message\ServerRequestFactoryInterface;
1010
use Psr\Http\Message\StreamFactoryInterface;
11+
use Psr\Http\Message\StreamInterface;
1112

1213
class RequestBuilder
1314
{
@@ -24,35 +25,31 @@ public function __construct(
2425
}
2526

2627
/**
27-
* @param array $options['headers']
28-
* @param array $options['query']
29-
* @param StreamInterface $options['stream']
30-
* @param string $options['body']
31-
* @param array $options['json']
32-
* @param array $options['multipart']
33-
* @param array $options['form']
28+
* @param array<string, mixed> $options
3429
*/
3530
public function build(string $method, string $url, array $options = []): RequestInterface
3631
{
3732
$request = $this->serverRequestFactory->createServerRequest(strtoupper($method), $url);
3833

39-
if (isset($options['headers'])) {
34+
if (isset($options['headers']) && is_array($options['headers'])) {
4035
foreach ($options['headers'] as $name => $value) {
4136
$request = $request->withHeader($name, $value);
4237
}
4338
}
4439

45-
if (isset($options['query'])) {
46-
$uri = $request->getUri()
47-
->withQuery(http_build_query($options['query']));
40+
$encoding = isset($options['encoding']) && is_int($options['encoding']) ? $options['encoding'] : PHP_QUERY_RFC1738;
41+
42+
if (isset($options['query']) && is_array($options['query'])) {
43+
$queryString = http_build_query($options['query'], encoding_type: $encoding);
44+
$uri = $request->getUri()->withQuery($queryString);
4845
$request = $request->withUri($uri);
4946
}
5047

51-
if (isset($options['stream'])) {
48+
if (isset($options['stream']) && $options['stream'] instanceof StreamInterface) {
5249
$request = $request->withBody($options['stream']);
5350
}
5451

55-
if (isset($options['body'])) {
52+
if (isset($options['body']) && is_string($options['body'])) {
5653
$body = $this->streamFactory->createStream($options['body']);
5754
$request = $request->withBody($body);
5855
}
@@ -66,7 +63,7 @@ public function build(string $method, string $url, array $options = []): Request
6663
$request = $request->withBody($body);
6764
}
6865

69-
if (isset($options['multipart'])) {
66+
if (isset($options['multipart']) && is_array($options['multipart'])) {
7067
$multipart = new MultipartBuilder($this->streamFactory);
7168

7269
foreach ($options['multipart'] as $name => $value) {
@@ -76,8 +73,9 @@ public function build(string $method, string $url, array $options = []): Request
7673
$request = $multipart->attach($request);
7774
}
7875

79-
if (isset($options['form'])) {
80-
$body = $this->streamFactory->createStream(http_build_query($options['form']));
76+
if (isset($options['form']) && is_array($options['form'])) {
77+
$queryString = http_build_query($options['form'], encoding_type: $encoding);
78+
$body = $this->streamFactory->createStream($queryString);
8179
$request = $request
8280
->withBody($body)
8381
->withHeader('Content-Type', 'application/x-www-form-urlencoded')

0 commit comments

Comments
 (0)