src/Shared/Infraestructure/Idempiere/IdempiereRestClient.php line 47

Open in your IDE?
  1. <?php
  2. namespace App\Shared\Infraestructure\Idempiere;
  3. use Symfony\Component\HttpFoundation\RequestStack;
  4. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  5. /**
  6. * @author rbellorin@sumagroups.com
  7. * @version 2026-03-03
  8. */
  9. final class IdempiereRestClient
  10. {
  11. public function __construct(
  12. private readonly RequestStack $requestStack
  13. ) {}
  14. /** @return array{status:int, body:string} */
  15. public function get(string $path, array $query = [], string $bearerToken = ''): array
  16. {
  17. $baseUrl = $this->normalizeBaseUrl((string)($_ENV['IDEMPIERE_BASE_URL'] ?? ''));
  18. if ($baseUrl === '') return ['status' => 0, 'body' => 'IDEMPIERE_BASE_URL vacío'];
  19. $session = $this->requestStack->getSession();
  20. $token = $bearerToken !== '' ? $bearerToken : (string) $session->get('idempiere.token', '');
  21. if ($token === '') return ['status' => 0, 'body' => 'idempiere.token vacío en sesión'];
  22. $url = $baseUrl . $this->normalizePath($path);
  23. if (!empty($query)) {
  24. // IMPORTANTE: OData keys ($filter, $top, etc.) deben quedar tal cual
  25. $url .= (str_contains($url, '?') ? '&' : '?')
  26. . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
  27. }
  28. return $this->requestWithSessionAuth('GET', $url, $token, $session);
  29. }
  30. /** @return array{status:int, body:string} */
  31. public function post(string $path, array $payload = []): array
  32. {
  33. $baseUrl = $this->normalizeBaseUrl((string)($_ENV['IDEMPIERE_BASE_URL'] ?? ''));
  34. if ($baseUrl === '') return ['status' => 0, 'body' => 'IDEMPIERE_BASE_URL vacío'];
  35. $session = $this->requestStack->getSession();
  36. $token = (string) $session->get('idempiere.token', '');
  37. if ($token === '') return ['status' => 0, 'body' => 'idempiere.token vacío en sesión'];
  38. $url = $baseUrl . $this->normalizePath($path);
  39. return $this->requestWithSessionAuth('POST', $url, $token, $session, $payload);
  40. }
  41. /** @return array<int, mixed> */
  42. public function decodeRecords(string $body): array
  43. {
  44. $data = json_decode($body, true);
  45. if (!is_array($data)) return [];
  46. if (isset($data['records']) && is_array($data['records'])) return $data['records'];
  47. if (isset($data['data']) && is_array($data['data'])) return $data['data'];
  48. // a veces el endpoint devuelve array plano
  49. return $data;
  50. }
  51. /** @return array{status:int, body:string} */
  52. private function requestWithSessionAuth(
  53. string $method,
  54. string $url,
  55. string $token,
  56. SessionInterface $session,
  57. array $payload = []
  58. ): array {
  59. $res = $this->requestJson($method, $url, $token, $payload);
  60. if (($res['status'] ?? 0) !== 401) {
  61. return $res;
  62. }
  63. $baseUrl = $this->normalizeBaseUrl((string)($_ENV['IDEMPIERE_BASE_URL'] ?? ''));
  64. if ($baseUrl === '') {
  65. return $res;
  66. }
  67. if (!$this->tryRefreshToken($baseUrl, $session)) {
  68. return $res;
  69. }
  70. $newToken = (string)$session->get('idempiere.token', '');
  71. if ($newToken === '') {
  72. return $res;
  73. }
  74. return $this->requestJson($method, $url, $newToken, $payload);
  75. }
  76. /** @return array{status:int, body:string} */
  77. private function requestJson(string $method, string $url, string $token, array $payload = []): array
  78. {
  79. $verifyPeer = $this->envBool('IDEMPIERECURLOPT_SSL_VERIFYPEER', false);
  80. $ch = curl_init($url);
  81. $options = [
  82. CURLOPT_RETURNTRANSFER => true,
  83. CURLOPT_HTTPHEADER => [
  84. 'Accept: application/json',
  85. 'Authorization: Bearer ' . $token,
  86. 'Expect:',
  87. ],
  88. CURLOPT_TIMEOUT => 20,
  89. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  90. CURLOPT_SSL_VERIFYPEER => $verifyPeer,
  91. CURLOPT_SSL_VERIFYHOST => $verifyPeer ? 2 : 0,
  92. ];
  93. if (strtoupper($method) === 'POST') {
  94. $options[CURLOPT_POST] = true;
  95. $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
  96. $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  97. }
  98. curl_setopt_array($ch, $options);
  99. $body = curl_exec($ch);
  100. $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  101. if ($body === false) {
  102. $error = curl_error($ch);
  103. curl_close($ch);
  104. return ['status' => 0, 'body' => $error !== '' ? $error : 'curl_exec devolvió false'];
  105. }
  106. curl_close($ch);
  107. return ['status' => $status, 'body' => (string)$body];
  108. }
  109. private function tryRefreshToken(string $baseUrl, SessionInterface $session): bool
  110. {
  111. $refreshToken = (string)$session->get('idempiere.refresh_token', '');
  112. if ($refreshToken === '') {
  113. return false;
  114. }
  115. $refreshPath = $this->normalizePath((string)($_ENV['IDEMPIERE_REFRESH_PATH'] ?? '/api/v1/auth/refresh'));
  116. $fallbackPath = $this->normalizePath((string)($_ENV['IDEMPIERE_REFRESH_FALLBACK_PATH'] ?? '/api/v1/auth/tokens/refresh'));
  117. $paths = $refreshPath === $fallbackPath ? [$refreshPath] : [$refreshPath, $fallbackPath];
  118. $currentToken = (string)$session->get('idempiere.token', '');
  119. $payloads = [
  120. ['refresh_token' => $refreshToken],
  121. ['refreshToken' => $refreshToken],
  122. ['token' => $currentToken, 'refresh_token' => $refreshToken],
  123. ];
  124. foreach ($paths as $path) {
  125. $url = $baseUrl . $path;
  126. foreach ($payloads as $payload) {
  127. $res = $this->requestJsonPublic('POST', $url, $payload);
  128. $status = (int)($res['status'] ?? 0);
  129. if ($status !== 200 && $status !== 201) {
  130. continue;
  131. }
  132. $data = json_decode((string)($res['body'] ?? ''), true);
  133. if (!is_array($data)) {
  134. continue;
  135. }
  136. $newToken = (string)($data['token'] ?? $data['access_token'] ?? '');
  137. if ($newToken === '') {
  138. continue;
  139. }
  140. $newRefresh = (string)($data['refresh_token'] ?? $data['refreshToken'] ?? $refreshToken);
  141. $session->set('idempiere.token', $newToken);
  142. $session->set('idempiere.refresh_token', $newRefresh);
  143. return true;
  144. }
  145. }
  146. return false;
  147. }
  148. /** @return array{status:int, body:string} */
  149. private function requestJsonPublic(string $method, string $url, array $payload = []): array
  150. {
  151. $verifyPeer = $this->envBool('IDEMPIERECURLOPT_SSL_VERIFYPEER', false);
  152. $ch = curl_init($url);
  153. $options = [
  154. CURLOPT_RETURNTRANSFER => true,
  155. CURLOPT_HTTPHEADER => [
  156. 'Accept: application/json',
  157. 'Expect:',
  158. ],
  159. CURLOPT_TIMEOUT => 20,
  160. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  161. CURLOPT_SSL_VERIFYPEER => $verifyPeer,
  162. CURLOPT_SSL_VERIFYHOST => $verifyPeer ? 2 : 0,
  163. ];
  164. if (strtoupper($method) === 'POST') {
  165. $options[CURLOPT_POST] = true;
  166. $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
  167. $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  168. }
  169. curl_setopt_array($ch, $options);
  170. $body = curl_exec($ch);
  171. $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  172. if ($body === false) {
  173. $error = curl_error($ch);
  174. curl_close($ch);
  175. return ['status' => 0, 'body' => $error !== '' ? $error : 'curl_exec devolvió false'];
  176. }
  177. curl_close($ch);
  178. return ['status' => $status, 'body' => (string)$body];
  179. }
  180. private function normalizeBaseUrl(string $raw): string
  181. {
  182. $raw = trim($raw);
  183. if ($raw === '') return '';
  184. $raw = rtrim($raw, '/');
  185. if (!preg_match('#^https?://#i', $raw)) $raw = 'https://' . $raw;
  186. return $raw;
  187. }
  188. private function normalizePath(string $path): string
  189. {
  190. $path = trim($path);
  191. if ($path === '') return '/';
  192. return $path[0] === '/' ? $path : '/' . $path;
  193. }
  194. private function envBool(string $key, bool $default): bool
  195. {
  196. $v = $_ENV[$key] ?? null;
  197. if ($v === null) return $default;
  198. if (is_string($v)) {
  199. $v = strtolower(trim($v));
  200. if (in_array($v, ['1', 'true', 'yes', 'on'], true)) return true;
  201. if (in_array($v, ['0', 'false', 'no', 'off', ''], true)) return false;
  202. }
  203. return (bool)$v;
  204. }
  205. }