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