1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* API Gateway: Cliente de API en PHP. |
7
|
|
|
* Copyright (C) API Gateway <https://www.apigateway.cl> |
8
|
|
|
* |
9
|
|
|
* Este programa es software libre: usted puede redistribuirlo y/o modificarlo |
10
|
|
|
* bajo los términos de la GNU Lesser General Public License (LGPL) publicada |
11
|
|
|
* por la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, |
12
|
|
|
* o (a su elección) cualquier versión posterior de la misma. |
13
|
|
|
* |
14
|
|
|
* Este programa se distribuye con la esperanza de que sea útil, pero SIN |
15
|
|
|
* GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD |
16
|
|
|
* PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la GNU Lesser General |
17
|
|
|
* Public License (LGPL) para obtener una información más detallada. |
18
|
|
|
* |
19
|
|
|
* Debería haber recibido una copia de la GNU Lesser General Public License |
20
|
|
|
* (LGPL) junto a este programa. En caso contrario, consulte |
21
|
|
|
* <http://www.gnu.org/licenses/lgpl.html>. |
22
|
|
|
*/ |
23
|
|
|
|
24
|
|
|
namespace apigatewaycl\api_client; |
25
|
|
|
|
26
|
|
|
use Psr\Http\Message\ResponseInterface; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Cliente de la API de API Gateway. |
30
|
|
|
* |
31
|
|
|
* Esta clase permite interactuar con la API de API Gateway, manejando |
32
|
|
|
* las solicitudes HTTP, la autenticación y el manejo de respuestas. |
33
|
|
|
*/ |
34
|
|
|
class ApiClient |
35
|
|
|
{ |
36
|
|
|
/** |
37
|
|
|
* La URL base de la API de API Gateway. |
38
|
|
|
* |
39
|
|
|
* @var string |
40
|
|
|
*/ |
41
|
|
|
private $api_url = 'https://legacy.apigateway.cl'; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* El prefijo para las rutas de la API. |
45
|
|
|
* |
46
|
|
|
* @var string |
47
|
|
|
*/ |
48
|
|
|
private $api_prefix = '/api'; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* La versión de la API a utilizar. |
52
|
|
|
* |
53
|
|
|
* @var string |
54
|
|
|
*/ |
55
|
|
|
private $api_version = 'v1'; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* El token de autenticación para la API. |
59
|
|
|
* |
60
|
|
|
* @var string|null |
61
|
|
|
*/ |
62
|
|
|
private $api_token = null; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* La última URL utilizada en la solicitud HTTP. |
66
|
|
|
* |
67
|
|
|
* @var string|null |
68
|
|
|
*/ |
69
|
|
|
private $last_url = null; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* La última respuesta recibida de la API. |
73
|
|
|
* |
74
|
|
|
* @var \Psr\Http\Message\ResponseInterface|null |
75
|
|
|
*/ |
76
|
|
|
private $last_response = null; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Constructor del cliente de la API. |
80
|
|
|
* |
81
|
|
|
* @param string|null $token Token de autenticación para la API. |
82
|
|
|
* @param string|null $url URL base de la API. |
83
|
|
|
*/ |
84
|
|
|
public function __construct(string $token = null, string $url = null) |
85
|
|
|
{ |
86
|
|
|
$this->api_token = $token ?: $this->env('APIGATEWAY_API_TOKEN'); |
87
|
|
|
if (!$this->api_token) { |
88
|
|
|
throw new ApiException(message: 'APIGATEWAY_API_TOKEN missing'); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
$this->api_url = $url ?: $this->env( |
92
|
|
|
'APIGATEWAY_API_URL' |
93
|
|
|
) ?: $this->api_url; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Establece la URL base de la API. |
98
|
|
|
* |
99
|
|
|
* @param string $url URL base. |
100
|
|
|
* @return $this |
101
|
|
|
*/ |
102
|
|
|
public function setUrl(string $url): static |
103
|
|
|
{ |
104
|
|
|
$this->api_url = $url; |
105
|
|
|
return $this; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Establece el token de autenticación. |
110
|
|
|
* |
111
|
|
|
* @param string $token Token de autenticación. |
112
|
|
|
* @return $this |
113
|
|
|
*/ |
114
|
|
|
public function setToken(string $token): static |
115
|
|
|
{ |
116
|
|
|
$this->api_token = $token; |
117
|
|
|
return $this; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Obtiene la última URL utilizada en la solicitud HTTP. |
122
|
|
|
* |
123
|
|
|
* @return string|null |
124
|
|
|
*/ |
125
|
|
|
public function getLastUrl(): string|null |
126
|
|
|
{ |
127
|
|
|
return $this->last_url; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Obtiene la última respuesta recibida de la API. |
132
|
|
|
* |
133
|
|
|
* @return \Psr\Http\Message\ResponseInterface|null |
134
|
|
|
*/ |
135
|
|
|
public function getLastResponse(): ResponseInterface|null |
136
|
|
|
{ |
137
|
|
|
return $this->last_response; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Obtiene el cuerpo de la última respuesta HTTP. |
142
|
|
|
* |
143
|
|
|
* Este método devuelve el cuerpo de la respuesta de la última |
144
|
|
|
* solicitud HTTP realizada utilizando este cliente API. |
145
|
|
|
* |
146
|
|
|
* @return string El cuerpo de la respuesta HTTP. |
147
|
|
|
* @throws ApiException Si no hay respuesta previa o el cuerpo no se puede obtener. |
148
|
|
|
*/ |
149
|
|
|
public function getBody(): string |
150
|
|
|
{ |
151
|
|
|
if (!$this->last_response) { |
152
|
|
|
throw new ApiException( |
153
|
|
|
message: 'No hay una respuesta HTTP previa para obtener el cuerpo.' |
154
|
|
|
); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
return (string)$this->last_response->getBody(); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Obtiene el cuerpo de la última respuesta HTTP y lo decodifica de JSON. |
162
|
|
|
* |
163
|
|
|
* Este método devuelve el cuerpo de la respuesta de la última |
164
|
|
|
* solicitud HTTP realizada por este cliente API, decodificándolo de |
165
|
|
|
* formato JSON a un arreglo asociativo de PHP. |
166
|
|
|
* |
167
|
|
|
* @return array El cuerpo de la respuesta HTTP decodificado como un arreglo. |
168
|
|
|
* @throws ApiException Si no hay respuesta previa o el cuerpo no se |
169
|
|
|
* puede decodificar. |
170
|
|
|
*/ |
171
|
|
|
public function getBodyDecoded(): mixed |
172
|
|
|
{ |
173
|
|
|
$body = $this->getBody(); |
174
|
|
|
|
175
|
|
|
$decodedBody = json_decode(json: $body, associative: true); |
176
|
|
|
|
177
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) { |
178
|
|
|
throw new ApiException( |
179
|
|
|
message: 'Error al decodificar JSON: ' . json_last_error_msg() |
180
|
|
|
); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
return $decodedBody; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Convierte la última respuesta HTTP en un arreglo asociativo. |
188
|
|
|
* |
189
|
|
|
* Este método transforma la última respuesta HTTP recibida en un arreglo |
190
|
|
|
* asociativo, que incluye información del estado HTTP, encabezados y el |
191
|
|
|
* cuerpo de la respuesta, ya sea en formato de texto o decodificado de JSON. |
192
|
|
|
* |
193
|
|
|
* @return array Arreglo asociativo con la información de la respuesta. |
194
|
|
|
* @throws ApiException Si se encuentra un error en el proceso. |
195
|
|
|
*/ |
196
|
|
|
public function toArray(): array |
197
|
|
|
{ |
198
|
|
|
if (!$this->last_response) { |
199
|
|
|
throw new ApiException( |
200
|
|
|
message: 'No hay una respuesta HTTP previa para procesar.' |
201
|
|
|
); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
$headers = $this->getLastResponse()->getHeaders(); |
205
|
|
|
foreach ($headers as &$header) { |
206
|
|
|
$header = isset($header[1]) ? $header : $header[0]; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
$statusCode = $this->getLastResponse()->getStatusCode(); |
210
|
|
|
$contentType = $this->getLastResponse()->getHeader('content-type')[0]; |
211
|
|
|
|
212
|
|
|
// Procesar el cuerpo de la respuesta según el tipo de contenido |
213
|
|
|
if ($contentType == 'application/json') { |
214
|
|
|
$body = $this->getBodyDecoded(); |
215
|
|
|
if ($body === null) { |
216
|
|
|
$body = $this->getBody(); |
217
|
|
|
$body = $body ?: $this->getError()->message; |
218
|
|
|
} |
219
|
|
|
} else { |
220
|
|
|
$body = $this->getBody(); |
221
|
|
|
$body = $body ?: $this->getError()->message; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
// Manejar respuestas con error |
225
|
|
|
if ($statusCode != 200) { |
226
|
|
|
if (!empty($body['message'])) { |
227
|
|
|
$body = $body['message']; |
228
|
|
|
} elseif (!empty($body['exception'])) { |
229
|
|
|
$body = $this->getError()->message; |
230
|
|
|
} else { |
231
|
|
|
$body = 'Error no determinado: ' . json_encode($body); |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
return [ |
236
|
|
|
'status' => [ |
237
|
|
|
'protocol' => $this->getLastResponse()->getProtocolVersion(), |
238
|
|
|
'code' => $statusCode, |
239
|
|
|
'message' => $this->getLastResponse()->getReasonPhrase(), |
240
|
|
|
], |
241
|
|
|
'header' => $headers, |
242
|
|
|
'body' => $body, |
243
|
|
|
]; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Realiza una solicitud GET a la API. |
248
|
|
|
* |
249
|
|
|
* @param string $resource Recurso de la API al cual realizar la solicitud. |
250
|
|
|
* @param array $headers Encabezados adicionales para incluir en la solicitud. |
251
|
|
|
* @param array $options Arreglo con las opciones de la solicitud HTTP. |
252
|
|
|
* @return \Psr\Http\Message\ResponseInterface|null |
253
|
|
|
*/ |
254
|
|
|
public function get( |
255
|
|
|
string $resource, |
256
|
|
|
array $headers = [], |
257
|
|
|
array $options = [] |
258
|
|
|
): ResponseInterface|null { |
259
|
|
|
return $this->consume( |
260
|
|
|
resource: $resource, |
261
|
|
|
data: [], |
262
|
|
|
headers: $headers, |
263
|
|
|
method: 'GET', |
264
|
|
|
options: $options |
265
|
|
|
)->getLastResponse(); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Realiza una solicitud POST a la API. |
270
|
|
|
* |
271
|
|
|
* @param string $resource Recurso de la API al cual realizar la solicitud. |
272
|
|
|
* @param array $data Datos a enviar en la solicitud. |
273
|
|
|
* @param array $headers Encabezados adicionales para incluir en la solicitud. |
274
|
|
|
* @param array $options Arreglo con las opciones de la solicitud HTTP. |
275
|
|
|
* @return \Psr\Http\Message\ResponseInterface|null |
276
|
|
|
*/ |
277
|
|
|
public function post( |
278
|
|
|
string $resource, |
279
|
|
|
array $data, |
280
|
|
|
array $headers = [], |
281
|
|
|
array $options = [] |
282
|
|
|
): ResponseInterface|null { |
283
|
|
|
return $this->consume( |
284
|
|
|
resource: $resource, |
285
|
|
|
data: $data, |
286
|
|
|
headers: $headers, |
287
|
|
|
method: 'POST', |
288
|
|
|
options: $options |
289
|
|
|
)->getLastResponse(); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Realiza una solicitud PUT a la API. |
294
|
|
|
* |
295
|
|
|
* @param string $resource Recurso de la API al cual realizar la solicitud. |
296
|
|
|
* @param array $data Datos a enviar en la solicitud. |
297
|
|
|
* @param array $headers Encabezados adicionales para incluir en la solicitud. |
298
|
|
|
* @param array $options Arreglo con las opciones de la solicitud HTTP. |
299
|
|
|
* @return \Psr\Http\Message\ResponseInterface|null |
300
|
|
|
*/ |
301
|
|
|
public function put( |
302
|
|
|
string $resource, |
303
|
|
|
array $data, |
304
|
|
|
array $headers = [], |
305
|
|
|
array $options = [] |
306
|
|
|
): ResponseInterface|null { |
307
|
|
|
return $this->consume( |
308
|
|
|
resource: $resource, |
309
|
|
|
data: $data, |
310
|
|
|
headers: $headers, |
311
|
|
|
method: 'PUT', |
312
|
|
|
options: $options |
313
|
|
|
)->getLastResponse(); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Realiza una solicitud DELETE a la API. |
318
|
|
|
* |
319
|
|
|
* @param string $resource Recurso de la API al cual realizar la solicitud. |
320
|
|
|
* @param array $headers Encabezados adicionales para incluir en la solicitud. |
321
|
|
|
* @param array $options Arreglo con las opciones de la solicitud HTTP. |
322
|
|
|
* @return \Psr\Http\Message\ResponseInterface|null |
323
|
|
|
*/ |
324
|
|
|
public function delete( |
325
|
|
|
string $resource, |
326
|
|
|
array $headers = [], |
327
|
|
|
array $options = [] |
328
|
|
|
): ResponseInterface|null { |
329
|
|
|
return $this->consume( |
330
|
|
|
resource: $resource, |
331
|
|
|
data: [], |
332
|
|
|
headers: $headers, |
333
|
|
|
method: 'DELETE', |
334
|
|
|
options: $options |
335
|
|
|
)->getLastResponse(); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Realiza una solicitud HTTP a la API. |
340
|
|
|
* |
341
|
|
|
* Este método envía una solicitud HTTP a la API de API Gateway, utilizando |
342
|
|
|
* los parámetros especificados y manejando la autenticación y la respuesta. |
343
|
|
|
* |
344
|
|
|
* @param string $resource El recurso de la API al cual realizar la solicitud. |
345
|
|
|
* @param array $data Datos a enviar en la solicitud (para métodos POST y PUT). |
346
|
|
|
* @param array $headers Encabezados adicionales para incluir en la solicitud. |
347
|
|
|
* @param string|null $method Método HTTP a utilizar (GET, POST, PUT, DELETE). |
348
|
|
|
* @param array $options Arreglo con las opciones de la solicitud HTTP. |
349
|
|
|
* @return $this Instancia actual del cliente para encadenar llamadas. |
350
|
|
|
* @throws ApiException Si se produce un error en la solicitud. |
351
|
|
|
*/ |
352
|
|
|
public function consume( |
353
|
|
|
string $resource, |
354
|
|
|
array $data = [], |
355
|
|
|
array $headers = [], |
356
|
|
|
string $method = null, |
357
|
|
|
$options = [] |
358
|
|
|
): static { |
359
|
|
|
$this->last_response = null; |
360
|
|
|
if (!$this->api_token) { |
361
|
|
|
throw new ApiException( |
362
|
|
|
message: 'Falta especificar token para autenticación.', |
363
|
|
|
code: 400 |
364
|
|
|
); |
365
|
|
|
} |
366
|
|
|
$method = $method ?: ($data ? 'POST' : 'GET'); |
367
|
|
|
$client = new \GuzzleHttp\Client(); |
368
|
|
|
$this->last_url = $this->api_url . |
369
|
|
|
$this->api_prefix . |
370
|
|
|
'/'. |
371
|
|
|
$this->api_version . |
372
|
|
|
$resource; |
373
|
|
|
|
374
|
|
|
// preparar cabeceras que se usarán |
375
|
|
|
$options[\GuzzleHttp\RequestOptions::HEADERS] = array_merge([ |
376
|
|
|
'Authorization' => 'Bearer ' . $this->api_token, |
377
|
|
|
'Content-Type' => 'application/json', |
378
|
|
|
'Accept' => 'application/json', |
379
|
|
|
], $headers); |
380
|
|
|
|
381
|
|
|
// agregar datos de la consulta si se pasaron (POST o PUT) |
382
|
|
|
if ($data) { |
|
|
|
|
383
|
|
|
$options[\GuzzleHttp\RequestOptions::JSON] = $data; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
// Ejecutar consulta al SII. |
387
|
|
|
try { |
388
|
|
|
$this->last_response = $client->request( |
389
|
|
|
method: $method, |
390
|
|
|
uri: $this->last_url, |
391
|
|
|
options: $options |
392
|
|
|
); |
393
|
|
|
} catch (\GuzzleHttp\Exception\GuzzleException $e) { |
394
|
|
|
// Obtener la respuesta de la llamada. |
395
|
|
|
$this->last_response = $e->getResponse(); |
|
|
|
|
396
|
|
|
// Se lanza la excepción. |
397
|
|
|
$this->throwException(); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
// Entregar respuesta (contenida en el mismo objeto del cliente). |
401
|
|
|
return $this; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* Extrae información detallada sobre un error a partir de la última respuesta HTTP. |
406
|
|
|
* |
407
|
|
|
* Este método analiza la última respuesta HTTP para extraer información |
408
|
|
|
* detallada sobre un error que ocurrió durante la solicitud. Devuelve un |
409
|
|
|
* objeto con los detalles del error, incluyendo el código y el mensaje. |
410
|
|
|
* |
411
|
|
|
* @return object Detalles del error con propiedades 'code' y 'message'. |
412
|
|
|
*/ |
413
|
|
|
private function getError(): object |
414
|
|
|
{ |
415
|
|
|
$data = $this->getBodyDecoded(); |
416
|
|
|
$response = $this->getLastResponse(); |
417
|
|
|
$statusCode = $response ? $response->getStatusCode() : null; |
418
|
|
|
$reasonPhrase = $response ? $response->getReasonPhrase() : 'Sin respuesta'; |
419
|
|
|
|
420
|
|
|
if ($data) { |
421
|
|
|
$code = isset($data['code']) ? $data['code'] : $statusCode; |
422
|
|
|
$message = isset($data['message']) ? $data['message'] : $reasonPhrase; |
423
|
|
|
} else { |
424
|
|
|
$code = $statusCode; |
425
|
|
|
$message = $reasonPhrase; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
// Se maneja el caso donde no se encuentra un mensaje de error específico |
429
|
|
|
if (!$message || $message === '') { |
430
|
|
|
$message = '[API Gateway] Código HTTP ' . $code . ': ' . $reasonPhrase; |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
return (object)[ |
434
|
|
|
'code' => $code, |
435
|
|
|
'message' => $message, |
436
|
|
|
]; |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
/** |
440
|
|
|
* Lanza una ApiException con los detalles del último error. |
441
|
|
|
* |
442
|
|
|
* Este método utiliza la información obtenida del método getError() para |
443
|
|
|
* lanzar una ApiException con un mensaje de error detallado y un código |
444
|
|
|
* de error asociado. Se utiliza para manejar errores de la API de manera |
445
|
|
|
* uniforme en toda la clase. |
446
|
|
|
* |
447
|
|
|
* @throws ApiException Lanza una excepción con detalles del error. |
448
|
|
|
*/ |
449
|
|
|
private function throwException(): ApiException |
450
|
|
|
{ |
451
|
|
|
$error = $this->getError(); |
452
|
|
|
throw new ApiException(message: $error->message, code: $error->code); |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* Obtiene el valor de una variable de entorno. |
457
|
|
|
* |
458
|
|
|
* @param string $name Nombre de la variable de entorno. |
459
|
|
|
* @return string|null Valor de la variable de entorno o null si |
460
|
|
|
* no está definida. |
461
|
|
|
*/ |
462
|
|
|
private function env(string $name) |
463
|
|
|
{ |
464
|
|
|
return function_exists('env') ? env($name) : getenv($name); |
465
|
|
|
} |
466
|
|
|
} |
467
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.