ApiClient   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 146
c 1
b 0
f 0
dl 0
loc 468
ccs 0
cts 165
cp 0
rs 8.4
wmc 50

17 Methods

Rating   Name   Duplication   Size   Complexity  
A env() 0 3 2
A throwException() 0 4 1
B consume() 0 53 8
A setToken() 0 4 1
B getError() 0 27 8
A setRut() 0 4 1
A setUrl() 0 4 1
A getBody() 0 9 2
A getLastUrl() 0 3 1
B __construct() 0 17 7
B toArray() 0 51 11
A delete() 0 12 1
A get() 0 12 1
A post() 0 13 1
A getBodyDecoded() 0 14 2
A getLastResponse() 0 3 1
A put() 0 13 1

How to fix   Complexity   

Complex Class

Complex classes like ApiClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiClient, and based on these observations, apply Extract Interface, too.

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

426
            /** @scrutinizer ignore-call */ 
427
            $this->last_response = $e->getResponse();
Loading history...
427
            $this->throwException();
428
        }
429
        if ($this->getLastResponse()->getStatusCode() != 200) {
430
            $this->throwException();
431
        }
432
        return $this;
433
    }
434
435
    /**
436
     * Extrae información detallada sobre un error a partir de la última respuesta HTTP.
437
     *
438
     * Este método analiza la última respuesta HTTP para extraer información
439
     * detallada sobre un error que ocurrió durante la solicitud. Devuelve un
440
     * objeto con los detalles del error, incluyendo el código y el mensaje.
441
     *
442
     * @return object Detalles del error con propiedades 'code' y 'message'.
443
     */
444
    private function getError()
445
    {
446
        $data = $this->getBodyDecoded();
447
        $response = $this->getLastResponse();
448
        $statusCode = $response ? $response->getStatusCode() : null;
449
        $reasonPhrase = $response ? $response->getReasonPhrase() : 'Sin respuesta';
450
451
        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...
452
            $code = isset($data['code']) ? $data['code'] : $statusCode;
453
            $message = isset($data['message']) ? $data['message'] : $reasonPhrase;
454
        } else {
455
            $code = $statusCode;
456
            $message = $reasonPhrase;
457
        }
458
459
        // Se maneja el caso donde no se encuentra un mensaje de error específico
460
        if (!$message || $message === '') {
461
            $message = sprintf(
462
                '[BHExpress API] Código HTTP %d: %s',
463
                $code,
464
                $reasonPhrase
465
            );
466
        }
467
468
        return (object)[
469
            'code' => $code,
470
            'message' => $message,
471
        ];
472
    }
473
474
    /**
475
     * Lanza una ApiException con los detalles del último error.
476
     *
477
     * Este método utiliza la información obtenida del método getError() para
478
     * lanzar una ApiException con un mensaje de error detallado y un código
479
     * de error asociado. Se utiliza para manejar errores de la API de manera
480
     * uniforme en toda la clase.
481
     *
482
     * @throws ApiException Lanza una excepción con detalles del error.
483
     */
484
    private function throwException()
485
    {
486
        $error = $this->getError();
487
        throw new ApiException($error->message, $error->code);
488
    }
489
490
    /**
491
     * Obtiene el valor de una variable de entorno.
492
     *
493
     * @param string $name Nombre de la variable de entorno.
494
     * @return string|null Valor de la variable de entorno o null si no está definida.
495
     */
496
    private function env(string $name)
497
    {
498
        return function_exists('env') ? env($name) : getenv($name);
499
    }
500
}
501