ApiClient::env()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 2
nc 2
nop 1
crap 6
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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();
0 ignored issues
show
Bug introduced by
The method getResponse() does not exist on GuzzleHttp\Exception\GuzzleException. It seems like you code against a sub-type of GuzzleHttp\Exception\GuzzleException such as GuzzleHttp\Exception\RequestException. ( Ignorable by Annotation )

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

395
            /** @scrutinizer ignore-call */ 
396
            $this->last_response = $e->getResponse();
Loading history...
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