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)) { |
|
|
|
|
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
|
|
|
|