Skip to content

Commit 0d0ac17

Browse files
committed
Refactor Httpie for improved request handling and introduce HttpResponse class
1 parent a22f658 commit 0d0ac17

8 files changed

Lines changed: 390 additions & 62 deletions

File tree

src/Command/MainCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ protected function execute(Input $input, Output $output): int
160160
private function checkUpdates()
161161
{
162162
try {
163-
fwrite(STDERR, Httpie::get('https://deployer.org/check-updates/' . DEPLOYER_VERSION)->send());
163+
fwrite(STDERR, Httpie::get('https://deployer.org/check-updates/' . DEPLOYER_VERSION)->send()->body());
164164
} catch (\Throwable $e) {
165165
// Meh
166166
}
@@ -177,7 +177,7 @@ private function showBanner()
177177
if (function_exists('posix_isatty') && posix_isatty(STDOUT)) {
178178
$withColors = '_with_colors';
179179
}
180-
fwrite(STDERR, Httpie::get("https://deployer.medv.io/banners/" . $this->getName() . $withColors)->send());
180+
fwrite(STDERR, Httpie::get("https://deployer.medv.io/banners/" . $this->getName() . $withColors)->send()->body());
181181
} catch (\Throwable $e) {
182182
// Meh
183183
}

src/Configuration.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,12 @@ public function load(): void
181181
}
182182

183183
$values = Httpie::post(MASTER_ENDPOINT . '/load')
184-
->setopt(CURLOPT_CONNECTTIMEOUT, 0)
185-
->setopt(CURLOPT_TIMEOUT, 0)
186-
->header('Authorization', 'Bearer ' . MASTER_TOKEN)
184+
->noTimeout()
185+
->bearerToken(MASTER_TOKEN)
187186
->jsonBody([
188187
'host' => $this->get('alias'),
189188
])
190-
->getJson();
189+
->sendJson();
191190
$this->update($values);
192191
}
193192

@@ -198,14 +197,13 @@ public function save(): void
198197
}
199198

200199
Httpie::post(MASTER_ENDPOINT . '/save')
201-
->setopt(CURLOPT_CONNECTTIMEOUT, 0)
202-
->setopt(CURLOPT_TIMEOUT, 0)
203-
->header('Authorization', 'Bearer ' . MASTER_TOKEN)
200+
->noTimeout()
201+
->bearerToken(MASTER_TOKEN)
204202
->jsonBody([
205203
'host' => $this->get('alias'),
206204
'config' => $this->persist(),
207205
])
208-
->getJson();
206+
->sendJson();
209207
}
210208

211209
public function persist(): array

src/Deployer.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,14 @@ public static function masterCall(Host $host, string $func, mixed ...$arguments)
315315
usleep(100_000); // Sleep 100ms.
316316

317317
return Httpie::post(MASTER_ENDPOINT . '/proxy')
318-
->setopt(CURLOPT_CONNECTTIMEOUT, 0) // no timeout
319-
->setopt(CURLOPT_TIMEOUT, 0) // no timeout
320-
->header('Authorization', 'Bearer ' . MASTER_TOKEN)
318+
->noTimeout()
319+
->bearerToken(MASTER_TOKEN)
321320
->jsonBody([
322321
'host' => $host->getAlias(),
323322
'func' => $func,
324323
'arguments' => $arguments,
325324
])
326-
->getJson();
325+
->sendJson();
327326
}
328327

329328
public static function isPharArchive(): bool

src/Utility/HttpResponse.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/* (c) Anton Medvedev <anton@medv.io>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace Deployer\Utility;
12+
13+
use Deployer\Exception\HttpieException;
14+
15+
class HttpResponse
16+
{
17+
private string $body;
18+
private array $info;
19+
20+
public function __construct(string $body, array $info)
21+
{
22+
$this->body = $body;
23+
$this->info = $info;
24+
}
25+
26+
public function body(): string
27+
{
28+
return $this->body;
29+
}
30+
31+
public function status(): int
32+
{
33+
return (int) ($this->info['http_code'] ?? 0);
34+
}
35+
36+
public function json(): mixed
37+
{
38+
return json_decode($this->body, true, flags: JSON_THROW_ON_ERROR);
39+
}
40+
41+
public function info(): array
42+
{
43+
return $this->info;
44+
}
45+
46+
public function __toString(): string
47+
{
48+
return $this->body;
49+
}
50+
}

src/Utility/Httpie.php

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
class Httpie
1616
{
17+
private static bool $extensionChecked = false;
18+
1719
private string $method = 'GET';
1820
private string $url = '';
1921
private array $headers = [];
@@ -23,48 +25,50 @@ class Httpie
2325

2426
public function __construct()
2527
{
26-
if (!extension_loaded('curl')) {
27-
throw new \Exception(
28-
"Please, install curl extension.\n"
29-
. "https://php.net/curl.installation",
30-
);
28+
if (!self::$extensionChecked) {
29+
if (!extension_loaded('curl')) {
30+
throw new \Exception(
31+
"Please, install curl extension.\n"
32+
. "https://php.net/curl.installation",
33+
);
34+
}
35+
self::$extensionChecked = true;
3136
}
3237
}
3338

34-
public static function get(string $url): Httpie
39+
public static function get(string $url): self
3540
{
3641
$http = new self();
3742
$http->method = 'GET';
3843
$http->url = $url;
3944
return $http;
4045
}
4146

42-
public static function post(string $url): Httpie
47+
public static function post(string $url): self
4348
{
4449
$http = new self();
4550
$http->method = 'POST';
4651
$http->url = $url;
4752
return $http;
4853
}
4954

50-
public static function patch(string $url): Httpie
55+
public static function patch(string $url): self
5156
{
5257
$http = new self();
5358
$http->method = 'PATCH';
5459
$http->url = $url;
5560
return $http;
5661
}
5762

58-
59-
public static function put(string $url): Httpie
63+
public static function put(string $url): self
6064
{
6165
$http = new self();
6266
$http->method = 'PUT';
6367
$http->url = $url;
6468
return $http;
6569
}
6670

67-
public static function delete(string $url): Httpie
71+
public static function delete(string $url): self
6872
{
6973
$http = new self();
7074
$http->method = 'DELETE';
@@ -74,7 +78,8 @@ public static function delete(string $url): Httpie
7478

7579
public function query(array $params): self
7680
{
77-
$this->url .= '?' . http_build_query($params);
81+
$separator = str_contains($this->url, '?') ? '&' : '?';
82+
$this->url .= $separator . http_build_query($params);
7883
return $this;
7984
}
8085

@@ -84,32 +89,52 @@ public function header(string $header, string $value): self
8489
return $this;
8590
}
8691

92+
public function bearerToken(string $token): self
93+
{
94+
$this->headers['Authorization'] = 'Bearer ' . $token;
95+
return $this;
96+
}
97+
98+
public function basicAuth(string $user, string $pass): self
99+
{
100+
$this->curlopts[CURLOPT_USERPWD] = "$user:$pass";
101+
return $this;
102+
}
103+
104+
public function timeout(int $seconds): self
105+
{
106+
$this->curlopts[CURLOPT_TIMEOUT] = $seconds;
107+
$this->curlopts[CURLOPT_CONNECTTIMEOUT] = $seconds;
108+
return $this;
109+
}
110+
111+
public function noTimeout(): self
112+
{
113+
$this->curlopts[CURLOPT_TIMEOUT] = 0;
114+
$this->curlopts[CURLOPT_CONNECTTIMEOUT] = 0;
115+
return $this;
116+
}
117+
87118
public function body(string $body): self
88119
{
89120
$this->body = $body;
90-
$this->headers = array_merge($this->headers, [
91-
'Content-Length' => strlen($this->body),
92-
]);
121+
$this->headers['Content-Length'] = (string) strlen($this->body);
93122
return $this;
94123
}
95124

96125
public function jsonBody(array $data): self
97126
{
98127
$this->body = json_encode($data, JSON_PRETTY_PRINT);
99-
$this->headers = array_merge($this->headers, [
100-
'Content-Type' => 'application/json',
101-
'Content-Length' => strlen($this->body),
102-
]);
128+
$this->headers['Content-Type'] = 'application/json';
129+
$this->headers['Content-Length'] = (string) strlen($this->body);
103130
return $this;
104131
}
105132

106133
public function formBody(array $data): self
107134
{
108135
$this->body = http_build_query($data);
109-
$this->headers = array_merge($this->headers, [
110-
'Content-type' => 'application/x-www-form-urlencoded',
111-
'Content-Length' => strlen($this->body),
112-
]);
136+
$this->headers['Content-type'] = 'application/x-www-form-urlencoded';
137+
$this->headers['Content-Length'] = (string) strlen($this->body);
113138
return $this;
114139
}
115140

@@ -129,9 +154,9 @@ public function nothrow(bool $on = true): self
129154
}
130155

131156
/**
132-
* @param-out array $info
157+
* Send the request and return a response object.
133158
*/
134-
public function send(?array &$info = null): string
159+
public function send(?array &$info = null): HttpResponse
135160
{
136161
if ($this->url === '') {
137162
throw new \RuntimeException('URL must not be empty to Httpie::send()');
@@ -157,26 +182,55 @@ public function send(?array &$info = null): string
157182
$info = curl_getinfo($ch) ?: [];
158183
if ($result === false) {
159184
if ($this->nothrow) {
160-
$result = '';
161-
} else {
162-
$error = curl_error($ch);
163-
$errno = curl_errno($ch);
164-
throw new HttpieException($error, $errno);
185+
return new HttpResponse('', $info);
165186
}
187+
$error = curl_error($ch);
188+
$errno = curl_errno($ch);
189+
throw new HttpieException($error, $errno);
166190
}
167-
return $result;
191+
return new HttpResponse($result, $info);
168192
}
169193

194+
/**
195+
* Send the request and return the decoded JSON response.
196+
*/
197+
public function sendJson(): mixed
198+
{
199+
return $this->send()->json();
200+
}
201+
202+
/**
203+
* @deprecated Use sendJson() instead.
204+
*/
170205
public function getJson(): mixed
171206
{
172-
$result = $this->send();
173-
$response = json_decode($result, true);
174-
if (json_last_error() !== JSON_ERROR_NONE) {
175-
throw new HttpieException(
176-
'JSON Error: ' . json_last_error_msg() . '\n'
177-
. 'Response: ' . $result,
178-
);
179-
}
180-
return $response;
207+
return $this->sendJson();
208+
}
209+
210+
// Getters for testing.
211+
212+
public function getMethod(): string
213+
{
214+
return $this->method;
215+
}
216+
217+
public function getUrl(): string
218+
{
219+
return $this->url;
220+
}
221+
222+
public function getHeaders(): array
223+
{
224+
return $this->headers;
225+
}
226+
227+
public function getBody(): string
228+
{
229+
return $this->body;
230+
}
231+
232+
public function getCurlopts(): array
233+
{
234+
return $this->curlopts;
181235
}
182236
}

src/functions.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,19 +1017,20 @@ function quote(string $arg): string
10171017
function fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string
10181018
{
10191019
$url = parse($url);
1020-
if (strtolower($method) === 'get') {
1021-
$http = Httpie::get($url);
1022-
} elseif (strtolower($method) === 'post') {
1023-
$http = Httpie::post($url);
1024-
} else {
1025-
throw new \InvalidArgumentException("Unknown method \"$method\".");
1026-
}
1020+
$http = match (strtolower($method)) {
1021+
'get' => Httpie::get($url),
1022+
'post' => Httpie::post($url),
1023+
'put' => Httpie::put($url),
1024+
'patch' => Httpie::patch($url),
1025+
'delete' => Httpie::delete($url),
1026+
default => throw new \InvalidArgumentException("Unknown method \"$method\"."),
1027+
};
10271028
$http = $http->nothrow($nothrow);
10281029
foreach ($headers as $key => $value) {
10291030
$http = $http->header($key, $value);
10301031
}
10311032
if ($body !== null) {
10321033
$http = $http->body($body);
10331034
}
1034-
return $http->send($info);
1035+
return $http->send($info)->body();
10351036
}

0 commit comments

Comments
 (0)