<?php
namespace App\Shared\Infraestructure\Idempiere;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* @author rbellorin@sumagroups.com
* @version 2026-03-03
*/
final class IdempiereRestClient
{
public function __construct(
private readonly RequestStack $requestStack
) {}
/** @return array{status:int, body:string} */
public function get(string $path, array $query = [], string $bearerToken = ''): array
{
$baseUrl = $this->normalizeBaseUrl((string)($_ENV['IDEMPIERE_BASE_URL'] ?? ''));
if ($baseUrl === '') return ['status' => 0, 'body' => 'IDEMPIERE_BASE_URL vacío'];
$session = $this->requestStack->getSession();
$token = $bearerToken !== '' ? $bearerToken : (string) $session->get('idempiere.token', '');
if ($token === '') return ['status' => 0, 'body' => 'idempiere.token vacío en sesión'];
$url = $baseUrl . $this->normalizePath($path);
if (!empty($query)) {
// IMPORTANTE: OData keys ($filter, $top, etc.) deben quedar tal cual
$url .= (str_contains($url, '?') ? '&' : '?')
. http_build_query($query, '', '&', PHP_QUERY_RFC3986);
}
return $this->requestWithSessionAuth('GET', $url, $token, $session);
}
/** @return array{status:int, body:string} */
public function post(string $path, array $payload = []): array
{
$baseUrl = $this->normalizeBaseUrl((string)($_ENV['IDEMPIERE_BASE_URL'] ?? ''));
if ($baseUrl === '') return ['status' => 0, 'body' => 'IDEMPIERE_BASE_URL vacío'];
$session = $this->requestStack->getSession();
$token = (string) $session->get('idempiere.token', '');
if ($token === '') return ['status' => 0, 'body' => 'idempiere.token vacío en sesión'];
$url = $baseUrl . $this->normalizePath($path);
return $this->requestWithSessionAuth('POST', $url, $token, $session, $payload);
}
/** @return array<int, mixed> */
public function decodeRecords(string $body): array
{
$data = json_decode($body, true);
if (!is_array($data)) return [];
if (isset($data['records']) && is_array($data['records'])) return $data['records'];
if (isset($data['data']) && is_array($data['data'])) return $data['data'];
// a veces el endpoint devuelve array plano
return $data;
}
/** @return array{status:int, body:string} */
private function requestWithSessionAuth(
string $method,
string $url,
string $token,
SessionInterface $session,
array $payload = []
): array {
$res = $this->requestJson($method, $url, $token, $payload);
if (($res['status'] ?? 0) !== 401) {
return $res;
}
$baseUrl = $this->normalizeBaseUrl((string)($_ENV['IDEMPIERE_BASE_URL'] ?? ''));
if ($baseUrl === '') {
return $res;
}
if (!$this->tryRefreshToken($baseUrl, $session)) {
return $res;
}
$newToken = (string)$session->get('idempiere.token', '');
if ($newToken === '') {
return $res;
}
return $this->requestJson($method, $url, $newToken, $payload);
}
/** @return array{status:int, body:string} */
private function requestJson(string $method, string $url, string $token, array $payload = []): array
{
$verifyPeer = $this->envBool('IDEMPIERECURLOPT_SSL_VERIFYPEER', false);
$ch = curl_init($url);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Authorization: Bearer ' . $token,
'Expect:',
],
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSL_VERIFYPEER => $verifyPeer,
CURLOPT_SSL_VERIFYHOST => $verifyPeer ? 2 : 0,
];
if (strtoupper($method) === 'POST') {
$options[CURLOPT_POST] = true;
$options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
$options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false) {
$error = curl_error($ch);
curl_close($ch);
return ['status' => 0, 'body' => $error !== '' ? $error : 'curl_exec devolvió false'];
}
curl_close($ch);
return ['status' => $status, 'body' => (string)$body];
}
private function tryRefreshToken(string $baseUrl, SessionInterface $session): bool
{
$refreshToken = (string)$session->get('idempiere.refresh_token', '');
if ($refreshToken === '') {
return false;
}
$refreshPath = $this->normalizePath((string)($_ENV['IDEMPIERE_REFRESH_PATH'] ?? '/api/v1/auth/refresh'));
$fallbackPath = $this->normalizePath((string)($_ENV['IDEMPIERE_REFRESH_FALLBACK_PATH'] ?? '/api/v1/auth/tokens/refresh'));
$paths = $refreshPath === $fallbackPath ? [$refreshPath] : [$refreshPath, $fallbackPath];
$currentToken = (string)$session->get('idempiere.token', '');
$payloads = [
['refresh_token' => $refreshToken],
['refreshToken' => $refreshToken],
['token' => $currentToken, 'refresh_token' => $refreshToken],
];
foreach ($paths as $path) {
$url = $baseUrl . $path;
foreach ($payloads as $payload) {
$res = $this->requestJsonPublic('POST', $url, $payload);
$status = (int)($res['status'] ?? 0);
if ($status !== 200 && $status !== 201) {
continue;
}
$data = json_decode((string)($res['body'] ?? ''), true);
if (!is_array($data)) {
continue;
}
$newToken = (string)($data['token'] ?? $data['access_token'] ?? '');
if ($newToken === '') {
continue;
}
$newRefresh = (string)($data['refresh_token'] ?? $data['refreshToken'] ?? $refreshToken);
$session->set('idempiere.token', $newToken);
$session->set('idempiere.refresh_token', $newRefresh);
return true;
}
}
return false;
}
/** @return array{status:int, body:string} */
private function requestJsonPublic(string $method, string $url, array $payload = []): array
{
$verifyPeer = $this->envBool('IDEMPIERECURLOPT_SSL_VERIFYPEER', false);
$ch = curl_init($url);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Expect:',
],
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSL_VERIFYPEER => $verifyPeer,
CURLOPT_SSL_VERIFYHOST => $verifyPeer ? 2 : 0,
];
if (strtoupper($method) === 'POST') {
$options[CURLOPT_POST] = true;
$options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json';
$options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false) {
$error = curl_error($ch);
curl_close($ch);
return ['status' => 0, 'body' => $error !== '' ? $error : 'curl_exec devolvió false'];
}
curl_close($ch);
return ['status' => $status, 'body' => (string)$body];
}
private function normalizeBaseUrl(string $raw): string
{
$raw = trim($raw);
if ($raw === '') return '';
$raw = rtrim($raw, '/');
if (!preg_match('#^https?://#i', $raw)) $raw = 'https://' . $raw;
return $raw;
}
private function normalizePath(string $path): string
{
$path = trim($path);
if ($path === '') return '/';
return $path[0] === '/' ? $path : '/' . $path;
}
private function envBool(string $key, bool $default): bool
{
$v = $_ENV[$key] ?? null;
if ($v === null) return $default;
if (is_string($v)) {
$v = strtolower(trim($v));
if (in_array($v, ['1', 'true', 'yes', 'on'], true)) return true;
if (in_array($v, ['0', 'false', 'no', 'off', ''], true)) return false;
}
return (bool)$v;
}
}