ConsumeWebserviceJob   A
last analyzed

Complexity

Total Complexity 16

Size/Duplication

Total Lines 252
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 16
eloc 88
dl 0
loc 252
ccs 0
cts 97
cp 0
rs 10
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A createSoapClientOptions() 0 39 2
A getWsdlUri() 0 25 3
B callServiceFunction() 0 104 9
A sendRequest() 0 30 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * LibreDTE: Biblioteca PHP (Núcleo).
7
 * Copyright (C) LibreDTE <https://www.libredte.cl>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (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 Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de
20
 * GNU junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace libredte\lib\Core\Package\Billing\Component\Integration\Worker\SiiLazy\Job;
26
27
use Derafu\Backbone\Abstract\AbstractJob;
28
use Derafu\Backbone\Attribute\Job;
29
use Derafu\Backbone\Contract\JobInterface;
30
use Derafu\Xml\Contract\XmlDocumentInterface;
31
use Derafu\Xml\XmlDocument;
32
use libredte\lib\Core\Package\Billing\Component\Integration\Contract\SiiRequestInterface;
33
use libredte\lib\Core\Package\Billing\Component\Integration\Exception\SiiConsumeWebserviceException;
34
use SoapClient;
35
use SoapFault;
36
37
/**
38
 * Clase para consumir los servicios web SOAP del SII.
39
 */
40
#[Job(name: 'consume_webservice', worker: 'sii_lazy', component: 'integration', package: 'billing')]
41
class ConsumeWebserviceJob extends AbstractJob implements JobInterface
42
{
43
    /**
44
     * Realiza una solicitud a un servicio web del SII mediante el uso de WSDL.
45
     *
46
     * Este método prepara y normaliza los datos recibidos y llama al método
47
     * que realmente hace la consulta al SII: callServiceFunction().
48
     *
49
     * @param SiiRequestInterface $request Datos de la solicitud al SII.
50
     * @param string $service Nombre del servicio del SII que se consumirá.
51
     * @param string $function Nombre de la función que se ejecutará en el
52
     * servicio web del SII.
53
     * @param array|int $args Argumentos que se pasarán al servicio web.
54
     * @param int|null $retry Intentos que se realizarán como máximo para
55
     * obtener respuesta.
56
     * @return XmlDocumentInterface Documento XML con la respuesta del servicio web.
57
     * @throws SiiConsumeWebserviceException En caso de error.
58
     */
59
    public function sendRequest(
60
        SiiRequestInterface $request,
61
        string $service,
62
        string $function,
63
        array|int $args = [],
64
        ?int $retry = null
65
    ): XmlDocumentInterface {
66
        // Revisar si se pasó en $args el valor de $retry.
67
        // @scrutinizer ignore-type-check
68
        if (is_int($args)) {
0 ignored issues
show
introduced by
The condition is_int($args) is always false.
Loading history...
69
            $retry = $args;
70
            $args = [];
71
        }
72
73
        // Definir el WSDL que se debe utilizar.
74
        $wsdl = $this->getWsdlUri($request, $service);
75
76
        // Resolver el valor de $retry.
77
        $retry = $request->getReintentos($retry);
78
79
        // Definir las opciones para consumir el servicio web.
80
        $soapClientOptions = $this->createSoapClientOptions($request);
81
82
        // Realizar la llamada a la función en el servicio web del SII.
83
        return $this->callServiceFunction(
84
            $wsdl,
85
            $function,
86
            $args,
87
            $soapClientOptions,
88
            $retry
89
        );
90
    }
91
92
    /**
93
     * Método para obtener el XML del WSDL (Web Services Description Language)
94
     * del servicio del SII que se desea consumir.
95
     *
96
     * @param SiiRequestInterface $request Datos de la solicitud al SII.
97
     * @param string $servicio Servicio para el cual se desea obtener su WSDL.
98
     * @return string Ubicación del WSDL del servicio según el ambiente que
99
     * esté configurado. Entrega, normalmente, un archivo local para un WSDL
100
     * del ambiente de certificación y siempre una URL para un WSDL del
101
     * ambiente de producción.
102
     */
103
    private function getWsdlUri(SiiRequestInterface $request, string $servicio): string
104
    {
105
        $ambiente = $request->getAmbiente();
106
107
        // Algunos WSDL del ambiente de certificación no funcionan tal cual los
108
        // provee SII. Lo anterior ya que apuntan a un servidor llamado
109
        // nogal.sii.cl el cual no es accesible desde Internet. Posiblemente es
110
        // un servidor local del SII para desarrollo. Así que LibreDTE tiene
111
        // para el ambiente de certificación WSDL modificados para funcionar
112
        // con el servidor de pruebas (maullin.sii.cl). Estos WSDL se usan
113
        // siempre al solicitar el WSDL del ambiente de certificación.
114
        // Cambios basados en: http://stackoverflow.com/a/28464354/3333009
115
        if ($ambiente->isCertificacion()) {
116
            $wsdl = $ambiente->getWsdlPath($servicio);
117
            if ($wsdl !== null) {
118
                return $wsdl;
119
            }
120
        }
121
122
        // Los WSDL para el ambiente de producción son directamente los
123
        // proporcionados por el SII y que están definidos en la configuración.
124
        // Si por cualquier motivo un WSDL de un servicio para el ambiente de
125
        // certificación no existe localmente en LibreDTE, también se entregará
126
        // el WSDL oficial del SII.
127
        return $ambiente->getWsdl($servicio);
128
    }
129
130
    /**
131
     * Ejecuta una función en un servicio web del SII mediante el uso de WSDL.
132
     *
133
     * @param string $wsdl WSDL del servicio del SII que se consumirá.
134
     * @param string $function Nombre de la función que se ejecutará,
135
     * @param array $args Argumentos que se pasarán al servicio web.
136
     * @param array $soapClientOptions Opciones del cliente SOAP.
137
     * @param int $retry Intentos que se realizarán como máximo.
138
     * @return XmlDocumentInterface Documento XML con la respuesta del servicio web.
139
     * @throws SiiConsumeWebserviceException En caso de error.
140
     */
141
    private function callServiceFunction(
142
        string $wsdl,
143
        string $function,
144
        array $args,
145
        array $soapClientOptions,
146
        int $retry
147
    ): XmlDocumentInterface {
148
        // Preparar cliente SOAP.
149
        try {
150
            $soap = new SoapClient($wsdl, $soapClientOptions);
151
        } catch (SoapFault $e) {
152
            $message = $e->getMessage();
153
            if (
154
                isset($e->getTrace()[0]['args'][1])
155
                && is_string($e->getTrace()[0]['args'][1])
156
            ) {
157
                $message .= ': ' . $e->getTrace()[0]['args'][1];
158
            }
159
            throw new SiiConsumeWebserviceException(sprintf(
160
                'Ocurrió un error al crear el cliente SOAP para la API del SII con el WSDL %s. %s',
161
                $wsdl,
162
                $message
163
            ));
164
        }
165
166
        // Argumentos adicionales para la llamada al servicio web SOAP mediante
167
        // __soapCall().
168
        $options = null;
169
170
        // En el WSDL indicadas como soap:header.
171
        $requestHeaders = [];
172
173
        // Si el SII enviase cabeceras SOAP devuelta.
174
        $responseHeaders = [];
175
176
        // Para almacenar la respuesta de la llamada a la API SOAP del SII.
177
        $responseBody = null;
178
179
        // Para ir almacenando los errores, si existen, de cada intento.
180
        $errors = [];
181
182
        // Ejecutar la función que se ha solicitado del servicio web a través
183
        // del cliente SOAP preparado previamente.
184
        // Se realizarán $retry intentos de consulta. O sea, si $retry > 0 se
185
        // hará una consulta más $retry - 1 reintentos.
186
        for ($i = 0; $i < $retry; $i++) {
187
            try {
188
                // Se realiza la llamada a la función en el servicio web.
189
                $responseBody = $soap->__soapCall(
190
                    $function,
191
                    $args,
192
                    $options,
193
                    $requestHeaders,
194
                    $responseHeaders
195
                );
196
                // Si la llamada no falló (no hubo excepción), se rompe el
197
                // ciclo de reintentos.
198
                break;
199
            } catch (SoapFault $e) {
200
                $message = $e->getMessage();
201
                if (
202
                    isset($e->getTrace()[0]['args'][1])
203
                    && is_string($e->getTrace()[0]['args'][1])
204
                ) {
205
                    $message .= ': ' . $e->getTrace()[0]['args'][1];
206
                }
207
                $errors[] = sprintf(
208
                    'Error al ejecutar la función %s en el servicio web SOAP del SII ($i = %d): %s',
209
                    $function,
210
                    $i,
211
                    $message
212
                );
213
                $responseBody = null;
214
                // El reitento será con "exponential backoff", por lo que se
215
                // hace una pausa de 0.2 * $retry segundos antes de volver a
216
                // intentar llamar a la función del servicio web.
217
                usleep(200000 * $retry);
218
            }
219
        }
220
221
        // Si la respuesta es `null` significa que ninguno de los intentos de
222
        // llamadas a la función del servicio web fue exitoso.
223
        if ($responseBody === null) {
224
            throw new SiiConsumeWebserviceException(sprintf(
225
                'No se obtuvo respuesta de la función %s del servicio web SOAP del SII después de %d intentos. %s',
226
                $function,
227
                $retry,
228
                implode(' ', $errors)
229
            ));
230
        }
231
232
        // El SII indica que la respuesta que envía es:
233
        //   Content-Type: text/xml;charset=utf-8
234
        // Sin embargo, parece que indica que es UTF-8 pero envía contenido
235
        // codificado con ISO-8859-1 (que es lo esperable del SII). Lo que hace
236
        // que los caracteres especiales se obtengan como "�", por lo cual se
237
        // reemplazan en la respuesta por "?" para que sea "un poco" más
238
        // legible la respuesta del SII.
239
        $responseBody = str_replace(['�', '�'], '?', $responseBody);
240
241
        // Entregar el resultado como un documento XML.
242
        $xmlDocument = new XmlDocument();
243
        $xmlDocument->loadXml($responseBody);
244
        return $xmlDocument;
245
    }
246
247
    /**
248
     * Define las opciones para consumir el servicio web del SII mediante SOAP.
249
     *
250
     * @param SiiRequestInterface $request Datos de la solicitud al SII.
251
     * @return array Arreglo con las opciones para SoapClient.
252
     */
253
    private function createSoapClientOptions(SiiRequestInterface $request): array
254
    {
255
        // Configuración de caché para SOAP.
256
        ini_set('soap.wsdl_cache_enabled', 3600);
257
        ini_set('soap.wsdl_cache_ttl', 3600);
258
259
        // Opciones base.
260
        $options = [
261
            'encoding' => 'ISO-8859-1',
262
            //'trace' => true, // Permite usar __getLastResponse().
263
            'exceptions' => true, // Lanza SoapFault en caso de error.
264
            'cache_wsdl' => WSDL_CACHE_MEMORY, // WSDL_CACHE_DISK o WSDL_CACHE_MEMORY.
265
            'keep_alive' => false,
266
            'stream_context' => [
267
                'http' => [
268
                    'header' => [
269
                        'User-Agent: Mozilla/5.0 (compatible; PROG 1.0; +https://www.libredte.cl)',
270
                    ],
271
                ],
272
            ],
273
        ];
274
275
        // Si no se debe verificar el certificado SSL del servidor del SII se
276
        // asigna al "stream_context" dicha configuración.
277
        if (!$request->getVerificarSsl()) {
278
            $options['stream_context']['ssl'] = [
279
                'verify_peer' => false,
280
                'verify_peer_name' => false,
281
                'allow_self_signed' => true,
282
            ];
283
        }
284
285
        // Crear el "stream context" verdadero con las opciones definidas.
286
        $options['stream_context'] = stream_context_create(
287
            $options['stream_context']
288
        );
289
290
        // Retornar las opciones para el SoapClient.
291
        return $options;
292
    }
293
}
294