Test Failed
Push — master ( e81f34...58150d )
by
unknown
19:06
created

ApiClient::toArray()   B

Complexity

Conditions 11
Paths 37

Size

Total Lines 47
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 30
c 1
b 0
f 0
nc 37
nop 0
dl 0
loc 47
ccs 0
cts 31
cp 0
crap 132
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
27
 * Cliente de la API de API Gateway.
28
 *
29
 * Esta clase permite interactuar con la API de API Gateway, manejando
30
 * las solicitudes HTTP, la autenticación y el manejo de respuestas.
31
 */
32
class ApiClient
33
{
34
    /**
35
     * La URL base de la API de API Gateway.
36
     *
37
     * @var string
38
     */
39
    private $api_url = 'https://apigateway.cl';
40
41
    /**
42
     * El prefijo para las rutas de la API.
43
     *
44
     * @var string
45
     */
46
    private $api_prefix = '/api';
47
48
    /**
49
     * La versión de la API a utilizar.
50
     *
51
     * @var string
52
     */
53
    private $api_version = 'v1';
54
55
    /**
56
     * El token de autenticación para la API.
57
     *
58
     * @var string|null
59
     */
60
    private $api_token = null;
61
62
    /**
63
     * La última URL utilizada en la solicitud HTTP.
64
     *
65
     * @var string|null
66
     */
67
    private $last_url = null;
68
69
    /**
70
     * La última respuesta recibida de la API.
71
     *
72
     * @var \Psr\Http\Message\ResponseInterface|null
73
     */
74
    private $last_response = null;
75
76
    /**
77
     * Constructor del cliente de la API.
78
     *
79
     * @param string|null $token Token de autenticación para la API.
80
     * @param string|null $url URL base de la API.
81
     */
82
    public function __construct(string $token = null, string $url = null)
83
    {
84
        $this->api_token = $token ?: $this->env('APIGATEWAY_API_TOKEN');
85
        if (!$this->api_token) {
86
            throw new ApiException('APIGATEWAY_API_TOKEN missing');
87
        }
88
89
        $this->api_url = $url ?: $this->env('APIGATEWAY_API_URL') ?: $this->api_url;
90
    }
91
92
    /**
93
     * Establece la URL base de la API.
94
     *
95
     * @param string $url URL base.
96
     * @return $this
97
     */
98
    public function setUrl(string $url)
99
    {
100
        $this->api_url = $url;
101
        return $this;
102
    }
103
104
    /**
105
     * Establece el token de autenticación.
106
     *
107
     * @param string $token Token de autenticación.
108
     * @return $this
109
     */
110
    public function setToken(string $token)
111
    {
112
        $this->api_token = $token;
113
        return $this;
114
    }
115
116
    /**
117
     * Obtiene la última URL utilizada en la solicitud HTTP.
118
     *
119
     * @return string|null
120
     */
121
    public function getLastUrl()
122
    {
123
        return $this->last_url;
124
    }
125
126
    /**
127
     * Obtiene la última respuesta recibida de la API.
128
     *
129
     * @return \Psr\Http\Message\ResponseInterface|null
130
     */
131
    public function getLastResponse()
132
    {
133
        return $this->last_response;
134
    }
135
136
    /**
137
     * Obtiene el cuerpo de la última respuesta HTTP.
138
     *
139
     * Este método devuelve el cuerpo de la respuesta de la última
140
     * solicitud HTTP realizada utilizando este cliente API.
141
     *
142
     * @return string El cuerpo de la respuesta HTTP.
143
     * @throws ApiException Si no hay respuesta previa o el cuerpo no se puede obtener.
144
     */
145
    public function getBody()
146
    {
147
        if (!$this->last_response) {
148
            throw new ApiException(
149
                'No hay una respuesta HTTP previa para obtener el cuerpo.'
150
            );
151
        }
152
153
        return (string)$this->last_response->getBody();
154
    }
155
156
    /**
157
     * Obtiene el cuerpo de la última respuesta HTTP y lo decodifica de JSON.
158
     *
159
     * Este método devuelve el cuerpo de la respuesta de la última
160
     * solicitud HTTP realizada por este cliente API, decodificándolo de
161
     * formato JSON a un arreglo asociativo de PHP.
162
     *
163
     * @return array El cuerpo de la respuesta HTTP decodificado como un arreglo.
164
     * @throws ApiException Si no hay respuesta previa o el cuerpo no se puede decodificar.
165
     */
166
    public function getBodyDecoded()
167
    {
168
        $body = $this->getBody();
169
170
        $decodedBody = json_decode($body, true);
171
172
        if (json_last_error() !== JSON_ERROR_NONE) {
173
            throw new ApiException(
174
                'Error al decodificar JSON: ' . json_last_error_msg()
175
            );
176
        }
177
178
        return $decodedBody;
179
    }
180
181
    /**
182
     * Convierte la última respuesta HTTP en un arreglo asociativo.
183
     *
184
     * Este método transforma la última respuesta HTTP recibida en un arreglo
185
     * asociativo, que incluye información del estado HTTP, encabezados y el
186
     * cuerpo de la respuesta, ya sea en formato de texto o decodificado de JSON.
187
     *
188
     * @return array Arreglo asociativo con la información de la respuesta.
189
     * @throws ApiException Si se encuentra un error en el proceso.
190
     */
191
    public function toArray()
192
    {
193
        if (!$this->last_response) {
194
            throw new ApiException(
195
                'No hay una respuesta HTTP previa para procesar.'
196
            );
197
        }
198
199
        $headers = $this->getLastResponse()->getHeaders();
200
        foreach ($headers as &$header) {
201
            $header = isset($header[1]) ? $header : $header[0];
202
        }
203
204
        $statusCode = $this->getLastResponse()->getStatusCode();
205
        $contentType = $this->getLastResponse()->getHeader('content-type')[0];
206
207
        // Procesar el cuerpo de la respuesta según el tipo de contenido
208
        if ($contentType == 'application/json') {
209
            $body = $this->getBodyDecoded();
210
            if ($body === null) {
0 ignored issues
show
introduced by
The condition $body === null is always false.
Loading history...
211
                $body = $this->getBody();
212
                $body = $body ?: $this->getError()->message;
213
            }
214
        } else {
215
            $body = $this->getBody();
216
            $body = $body ?: $this->getError()->message;
217
        }
218
219
        // Manejar respuestas con error
220
        if ($statusCode != 200) {
221
            if (!empty($body['message'])) {
222
                $body = $body['message'];
223
            } elseif (!empty($body['exception'])) {
224
                $body = $this->getError()->message;
225
            } else {
226
                $body = 'Error no determinado: ' . json_encode($body);
227
            }
228
        }
229
230
        return [
231
            'status' => [
232
                'protocol' => $this->getLastResponse()->getProtocolVersion(),
233
                'code' => $statusCode,
234
                'message' => $this->getLastResponse()->getReasonPhrase(),
235
            ],
236
            'header' => $headers,
237
            'body' => $body,
238
        ];
239
    }
240
241
    /**
242
     * Realiza una solicitud GET a la API.
243
     *
244
     * @param string $resource Recurso de la API al cual realizar la solicitud.
245
     * @param array $headers Encabezados adicionales para incluir en la solicitud.
246
     * @param array $options Arreglo con las opciones de la solicitud HTTP.
247
     * @return \Psr\Http\Message\ResponseInterface|null
248
     */
249
    public function get(
250
        string $resource,
251
        array $headers = [],
252
        array $options = []
253
    )
254
    {
255
        return $this->consume(
256
            $resource,
257
            [],
258
            $headers,
259
            'GET',
260
            $options
261
        )->getLastResponse();
262
    }
263
264
    /**
265
     * Realiza una solicitud POST a la API.
266
     *
267
     * @param string $resource Recurso de la API al cual realizar la solicitud.
268
     * @param array $data Datos a enviar en la solicitud.
269
     * @param array $headers Encabezados adicionales para incluir en la solicitud.
270
     * @param array $options Arreglo con las opciones de la solicitud HTTP.
271
     * @return \Psr\Http\Message\ResponseInterface|null
272
     */
273
    public function post(
274
        string $resource,
275
        array $data,
276
        array $headers = [],
277
        array $options = []
278
    )
279
    {
280
        return $this->consume(
281
            $resource,
282
            $data,
283
            $headers,
284
            'POST',
285
            $options
286
        )->getLastResponse();
287
    }
288
289
    /**
290
     * Realiza una solicitud PUT a la API.
291
     *
292
     * @param string $resource Recurso de la API al cual realizar la solicitud.
293
     * @param array $data Datos a enviar en la solicitud.
294
     * @param array $headers Encabezados adicionales para incluir en la solicitud.
295
     * @param array $options Arreglo con las opciones de la solicitud HTTP.
296
     * @return \Psr\Http\Message\ResponseInterface|null
297
     */
298
    public function put(
299
        string $resource,
300
        array $data,
301
        array $headers = [],
302
        array $options = []
303
    )
304
    {
305
        return $this->consume(
306
            $resource,
307
            $data,
308
            $headers,
309
            'PUT',
310
            $options
311
        )->getLastResponse();
312
    }
313
314
    /**
315
     * Realiza una solicitud DELETE a la API.
316
     *
317
     * @param string $resource Recurso de la API al cual realizar la solicitud.
318
     * @param array $headers Encabezados adicionales para incluir en la solicitud.
319
     * @param array $options Arreglo con las opciones de la solicitud HTTP.
320
     * @return \Psr\Http\Message\ResponseInterface|null
321
     */
322
    public function delete(
323
        string $resource,
324
        array $headers = [],
325
        array $options = []
326
    )
327
    {
328
        return $this->consume(
329
            $resource,
330
            [],
331
            $headers,
332
            'DELETE',
333
            $options
334
        )->getLastResponse();
335
    }
336
337
    /**
338
     * Realiza una solicitud HTTP a la API.
339
     *
340
     * Este método envía una solicitud HTTP a la API de API Gateway, utilizando
341
     * los parámetros especificados y manejando la autenticación y la respuesta.
342
     *
343
     * @param string $resource El recurso de la API al cual realizar la solicitud.
344
     * @param array $data Datos a enviar en la solicitud (para métodos POST y PUT).
345
     * @param array $headers Encabezados adicionales para incluir en la solicitud.
346
     * @param string|null $method Método HTTP a utilizar (GET, POST, PUT, DELETE).
347
     * @param array $options Arreglo con las opciones de la solicitud HTTP.
348
     * @return $this Instancia actual del cliente para encadenar llamadas.
349
     * @throws ApiException Si se produce un error en la solicitud.
350
     */
351
    public function consume(
352
        string $resource,
353
        array $data = [],
354
        array $headers = [],
355
        string $method = null,
356
        $options = []
357
    )
358
    {
359
        $this->last_response = null;
360
        if (!$this->api_token) {
361
            throw new ApiException(
362
                'Falta especificar token para autenticación.',
363
                400
364
            );
365
        }
366
        $method = $method ?: ($data ? 'POST' : 'GET');
367
        $client = new \GuzzleHttp\Client();
368
        $this->last_url = $this->api_url . $this->api_prefix . '/'. $this->api_version . $resource;
369
370
        // preparar cabeceras que se usarán
371
        $options[\GuzzleHttp\RequestOptions::HEADERS] = array_merge([
372
            'Authorization' => 'Bearer ' . $this->api_token,
373
            'Content-Type' => 'application/json',
374
            'Accept' => 'application/json',
375
        ], $headers);
376
377
        // agregar datos de la consulta si se pasaron (POST o PUT)
378
        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...
379
            $options[\GuzzleHttp\RequestOptions::JSON] = $data;
380
        }
381
382
        // Ejecutar consulta al SII.
383
        try {
384
            $this->last_response = $client->request(
385
                $method,
386
                $this->last_url,
387
                $options
388
            );
389
        } catch (\GuzzleHttp\Exception\GuzzleException $e) {
390
            // Obtener la respuesta de la llamada.
391
            $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

391
            /** @scrutinizer ignore-call */ 
392
            $this->last_response = $e->getResponse();
Loading history...
392
            // Se lanza la excepción.
393
            $this->throwException();
394
        }
395
396
        // Entregar respuesta (contenida en el mismo objeto del cliente).
397
        return $this;
398
    }
399
400
    /**
401
     * Extrae información detallada sobre un error a partir de la última respuesta HTTP.
402
     *
403
     * Este método analiza la última respuesta HTTP para extraer información
404
     * detallada sobre un error que ocurrió durante la solicitud. Devuelve un
405
     * objeto con los detalles del error, incluyendo el código y el mensaje.
406
     *
407
     * @return object Detalles del error con propiedades 'code' y 'message'.
408
     */
409
    private function getError()
410
    {
411
        $data = $this->getBodyDecoded();
412
        $response = $this->getLastResponse();
413
        $statusCode = $response ? $response->getStatusCode() : null;
414
        $reasonPhrase = $response ? $response->getReasonPhrase() : 'Sin respuesta';
415
416
        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...
417
            $code = isset($data['code']) ? $data['code'] : $statusCode;
418
            $message = isset($data['message']) ? $data['message'] : $reasonPhrase;
419
        } else {
420
            $code = $statusCode;
421
            $message = $reasonPhrase;
422
        }
423
424
        // Se maneja el caso donde no se encuentra un mensaje de error específico
425
        if (!$message || $message === '') {
426
            $message = '[API Gateway] Código HTTP ' . $code . ': ' . $reasonPhrase;
427
        }
428
429
        return (object)[
430
            'code' => $code,
431
            'message' => $message,
432
        ];
433
    }
434
435
    /**
436
     * Lanza una ApiException con los detalles del último error.
437
     *
438
     * Este método utiliza la información obtenida del método getError() para
439
     * lanzar una ApiException con un mensaje de error detallado y un código
440
     * de error asociado. Se utiliza para manejar errores de la API de manera
441
     * uniforme en toda la clase.
442
     *
443
     * @throws ApiException Lanza una excepción con detalles del error.
444
     */
445
    private function throwException()
446
    {
447
        $error = $this->getError();
448
        throw new ApiException($error->message, $error->code);
449
    }
450
451
    /**
452
     * Obtiene el valor de una variable de entorno.
453
     *
454
     * @param string $name Nombre de la variable de entorno.
455
     * @return string|null Valor de la variable de entorno o null si no está definida.
456
     */
457
    private function env(string $name)
458
    {
459
        return function_exists('env') ? env($name) : getenv($name);
460
    }
461
}
462