-
Notifications
You must be signed in to change notification settings - Fork 0
/
ClarusHttpClient.php
233 lines (189 loc) · 6.35 KB
/
ClarusHttpClient.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
<?php
declare(strict_types=1);
/*
* This file is part of clarus-it/http-client package.
*
* (c) PT Clarus Innovace Teknologi <https://clarus-it.co.id>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/
namespace ClarusIt\HttpClient;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token\Parser;
use Lcobucci\JWT\UnencryptedToken;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
final class ClarusHttpClient implements HttpClientInterface
{
const TIME_COMPENSATION = 300;
/**
* API Key yang didapatkan dari aplikasi
*/
private string $apiKey;
/**
* JWT token yang didapatkan melalui proses login di API
*/
private ?string $token = null;
/**
* Instance HttpClient yang digunakan untuk melakukan request
*/
private HttpClientInterface $client;
/**
* Parser untuk JWT token
*/
private Parser $parser;
/**
* @param string $apiKey API Key yang didapat dari aplikasi
* @param string $baseUrl Base URL, termasuk path /api/ di akhir, misalnya
* https://example.com/api/
* @param HttpClientInterface|null $client Jika menggunakan framework yang
* sudah memiliki instance HttpClientInterface, bisa diisi di sini, jika
* tidak ada juga tidak masalah.
*/
public function __construct(
string $apiKey,
string $baseUrl,
?HttpClientInterface $client = null
) {
$this->apiKey = $apiKey;
$this->client = $client ?? HttpClient::create();
$this->parser = new Parser(new JoseEncoder());
$this->client = $this->client->withOptions(
[
'base_uri' => $baseUrl,
'headers' => [
'user-agent' => 'ClarusHttpClient/0.5',
]
]
);
}
/**
* Melakukan request ke API. Jika mendapatkan response 401, maka akan
* otomatis melakukan login dan mengulangi request.
*
* @param array<array-key,mixed> $options
*/
public function request(
string $method,
string $url,
array $options = []
): ResponseInterface {
// jika belum memiliki token, lakukan login. jika login gagal, maka
// method `login()` akan throw Exception, sehingga method ini pun akan
// gagal dengan Exception tersebut.
if ($this->token === null) {
$this->login();
}
// jika token yang kita miliki sudah kedaluarsa atau sebentar lagi akan
// kedaluarsa, maka kita lakukan login ulang.
if ($this->isTokenExpired()) {
$this->login();
}
// lakukan request ke API berdasarkan method, url, dan options yang
// diberikan
$response = $this->getAuthenticatedClient()->request($method, $url, $options);
// jika status response adalah 401, maka token yang digunakan tidak
// valid, maka kita melakukan login ulang, dan mengulangi request yang
// sama.
if (401 === $response->getStatusCode()) {
$this->login();
$response = $this->getAuthenticatedClient()->request($method, $url, $options);
}
return $response;
}
/**
* HTTP client yang sudah dilengkapi dengan header Authorization berdasarkan
* token yang sudah didapatkan
*/
private function getAuthenticatedClient(): HttpClientInterface
{
if ($this->token === null) {
throw new \LogicException('Token tidak tersedia, silakan lakukan login() terlebih dahulu');
}
return $this->client->withOptions(
[
'headers' => [
'Authorization' => 'Bearer ' . $this->token,
]
]
);
}
/**
* Melakukan login ke API untuk mendapatkan token. Token lalu disimpan
* di property $token
*
* Jika login gagal, maka method ini akan throw Exception saat pemanggilan
* `toArray()`
*/
private function login(): void
{
$response = $this->client->request(
'POST',
'login',
[
'json' => [
'apikey' => $this->apiKey,
]
]
);
$data = $response->toArray();
$token = $data['token'] ?? null;
if (!is_string($token)) {
throw new \RuntimeException('Token is not a string');
}
$this->token = $token;
}
/**
* Menghitung apakah token yang kita miliki saat ini sudah kedaluarsa, atau
* sebentar lagi kedaluarsa
*/
private function isTokenExpired(): bool
{
// jika token tidak ada, maka dianggap sudah kedaluarsa
if ($this->token === null || $this->token === '') {
return true;
}
// parse token yang kita miliki
$token = $this->parser->parse($this->token);
// jika token yang kita miliki bukan instance dari UnencryptedToken,
// maka dianggap sudah kedaluarsa
if (!$token instanceof UnencryptedToken) {
return true;
}
// ambil claims dari token
$claims = $token->claims();
// ambil nilai `exp` dari claims
$exp = $claims->get('exp');
// jika nilai `exp` tidak ada, atau bukan integer, maka dianggap sudah
// kedaluarsa
if (!is_int($exp)) {
return true;
}
// jika waktu kedaluarsa dari token kurang dari waktu sekarang dikurangi
// dengan TIME_COMPENSATION, maka dianggap sudah kedaluarsa. jadi jika
// token expire pukul 10:00, maka token dianggap sudah kedaluarsa pukul
// 09:55
return $exp < time() - self::TIME_COMPENSATION;
}
/**
* Tidak digunakan, silakan diabaikan saja
*/
public function stream(
ResponseInterface|iterable $responses,
?float $timeout = null
): ResponseStreamInterface {
throw new \LogicException('Not implemented yet');
}
/**
* Tidak digunakan, silakan diabaikan saja
*
* @param array<array-key,mixed> $options
*/
public function withOptions(array $options): static
{
throw new \LogicException('Not implemented yet');
}
}