<?php
namespace App\Services;
use App\Controller\Log;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ApiConsumerService
{
protected $cache;
protected $em;
private $httpClient;
private $security;
private $translator;
protected $projectDir;
protected $logger;
public function __construct(
HttpClientInterface $hermesClient,
Security $security,
TranslatorInterface $translator,
EntityManagerInterface $em,
$projectDir,
LoggerInterface $cacheLogger
) {
$this->httpClient = $hermesClient;
$this->security = $security;
$this->translator = $translator;
$this->cache = new FilesystemAdapter('', 0, $projectDir."/var/cache");
$this->em = $em;
$this->projectDir = $projectDir;
$this->logger=$cacheLogger;
}
/**
* Hace una petición GET sin tener en cuenta la caché (sin ver si está en caché y sin almacenar).
* Se utiliza en peticiones de cambio como change-subscription-status que realmente no obtenemos un objeto cacheable.
*/
public function getDataNoCache($endpoint, $queryParams = [], $global=false, $expire=null, $headers=null) {
return $this->getData($endpoint, $queryParams = [], false, $global=false, $expire=null, $headers=null, false);
}
public function getData($endpoint, $queryParams = [], $fromCache = true, $global = false, $expire = null, $headers = null, $writeCache = true)
{
// Añadimos el parámetro '$gzip' para indicar el nivel de compresión (9 en este caso).
$queryParams['$gzip'] = 9;
// También calculamos el tiempo de expiración '$expire', utilizando un valor predeterminado
// de la configuración ($_ENV["EXPIRE"]) con una variación aleatoria del 20%.
$queryParams['$expire'] = $expire ?? $_ENV["EXPIRE"] + rand(-($expire ?? $_ENV["EXPIRE"] * 0.2), $expire ?? $_ENV["EXPIRE"] * 0.2);
// Limpiamos los parámetros de consulta ($queryParams) eliminando claves reservadas ('$gzip', '$expire', '$nocache').
// Esto evita que estas claves afecten la generación de la clave de caché o las consultas al API.
foreach (($cleanParams = $queryParams ?? []) as $parameter => $value) {
if (in_array($parameter, ['$gzip', '$expire', '$nocache'])) unset($cleanParams[$parameter]);
}
// Generamos una clave única para la caché ($key) utilizando:
// - La URL del endpoint.
// - Los parámetros de consulta limpiados.
// - Los encabezados proporcionados o predeterminados.
$key = md5($endpoint . json_encode($cleanParams) . json_encode($headers ?? $this->getHeaders($global)));
// Registramos en el log la clave generada y un resumen de los parámetros de consulta y encabezados.
$this->logger->debug("{$key} => {$endpoint} Q=(" . md5(json_encode($queryParams)) . ") H=(" . md5(json_encode($headers)) . ")");
// Recuperamos los elementos de caché para la clave específica ($key) y el historial de solicitudes.
$cacheResponse = $this->cache->getItem($key);
// Si el flag $fromCache es true, intentamos obtener la respuesta desde la caché.
//TODO Revisar no va bien la cache
if ($fromCache) {
$this->logger->debug("{$endpoint} Intentamos obtener respuesta desde la caché...");
// Si la respuesta está en caché, la devolvemos directamente.
$this->logger->debug("Cache content: " . json_encode($cacheResponse->get()));
if ($cacheResponse->isHit()) {
$this->logger->debug("Get from cache ...");
$this->updateCacheHistorico($cacheResponse, $key, $endpoint, $queryParams, $global, $headers);
$response = json_decode($cacheResponse->get(),true);
// DEBUG-INC666-G consumer-hydrate-evento-fields
// Log claves devueltas desde cache cuando endpoint es evento; permite detectar
// payload cacheado pre-fix sin los nuevos campos (is_publico, inscribir, fecha_*).
if (function_exists('error_log') && is_string($endpoint) && strpos($endpoint, 'evento/') !== false) {
$keysList = is_array($response) ? implode(',', array_keys($response)) : 'not-array';
error_log(sprintf('[DEBUG-INC666-G] ApiConsumerService cache-hit endpoint=%s keys=%s', $endpoint, $keysList));
}
if (@$response['status']>=300) {
if (@$response['status']==400) return null;
}
return $response['content'];
} else {
$queryParams['$nocache'] = true;
$this->logger->debug("No hay Hit -> salimos...");
}
} else {
// Si $fromCache es false, no intentamos recuperar datos de la caché.
$this->logger->debug("No cacheamos {$endpoint}.");
$queryParams['$nocache'] = true; // Forzamos una consulta nueva.
}
// Realizamos la solicitud HTTP GET utilizando los parámetros y encabezados proporcionados.
$this->logger->debug("Get via API ...");
$response = $this->httpClient->request(
"GET",
$endpoint,
[
'query' => $queryParams, // Parámetros de consulta.
'headers' => $headers ?? $this->getHeaders($global) // Encabezados.
]
);
// Obtenemos el contenido de la respuesta sin procesar.
$contenido = $response->getContent(false);
// Intentamos decodificar el contenido: primero como Base64 y luego descomprimirlo (gzip).
try {
if (($contenido = base64_decode($contenido)) === false) {
throw new \Exception("Error decode base 64"); // Error en la decodificación Base64.
}
if (($contenido = gzdecode($contenido)) === false) {
throw new \Exception("Error gzdecode"); // Error al descomprimir.
}
} catch (\Exception $e) {
// Si ocurre un error, usamos el contenido sin procesar.
$contenido = $response->getContent(false);
}
// Si el código de estado de la respuesta no es 200 (éxito):
if ($response->getStatusCode() === 200) {
if ($writeCache) {
$this->updateCacheHistorico($cacheResponse, $key, $endpoint, $queryParams, $global, $headers);
$this->cache->save(
$cacheResponse
->set(json_encode(['content' => $contenido, 'status' => $response->getStatusCode()]))
->expiresAfter($expire)
);
}
} else {
// Para otros errores, extraemos el mensaje del error (si existe) y lanzamos una excepción genérica.
$error = @(@json_decode($contenido, true))["error"] ?? $contenido;
if ($response->getStatusCode() == 400) return null;
if ($response->getStatusCode() > 400) throw new NotAcceptableHttpException($response->getStatusCode() . "::" . $error);
}
// Retornamos el contenido obtenido del API
return $contenido;
}
public function postData($endpoint, $postData, $queryParams = [])
{
if (!($decodedData = json_decode($postData, true))) {
throw new \InvalidArgumentException();
}
try {
$response = $this->httpClient->request(
"POST",
$endpoint,
[
'body' => $decodedData,
// necesario para hermes, hermes espera un content-type form-dat o url-conded
'headers' => $this->getHeaders(),
'query' => $queryParams,
]
);
} catch (\Exception $e) {
throw new NotAcceptableHttpException("{$e->getMessage()} ({$e->getFile()}:{$e->getLine()})");
}
if ($response->getStatusCode() != 200) {
$jsonError = json_decode($response->getContent(false), true);
$contenido = ($jsonError and @$jsonError['error']) ? $jsonError['error'] : $response->getContent(false);
throw new NotAcceptableHttpException("ENDPOINT $endpoint: $contenido");
}
return $response->getContent();
}
public function postMultiPartData($endpoint, $formFields = [])
{
$formFields['data']->imagen = $formFields['imagen'];
$formData = new FormDataPart($formFields['data']);
$headers = $formData->getPreparedHeaders()->toArray();
$headers = array_merge($headers, $this->getHeaders());
try {
$response = $this->httpClient->request(
"POST",
$endpoint,
[
'headers' => $headers,
'body' => $formData->bodyToIterable(),
]
);
} catch (\Exception $e) {
}
if ($response->getStatusCode() != 200) {
$contenido = $response->getContent(false);
throw new NotAcceptableHttpException($contenido);
}
return $response->getContent();
}
private function getHeaders($global=false)
{
$headers = [];
if ($this->security->getUser()?->getUserToken() and !$global) {
$headers['userToken'] = $this->security->getUser()->getUserToken();
} else {
$headers['entidadToken'] = $_ENV['WS_ENTIDAD_TOKEN'];
}
$headers['appToken'] = $_ENV['WS_APP_TOKEN'];
return $headers;
}
public function updateCacheHistorico(?CacheItem $cacheResponse, string $key, $endpoint, $queryParams, $global, $headers)
{
$cacheHistorico = $this->cache->getItem("HISTORICO");
$historico = @json_decode($cacheHistorico->get(), true) ?? [];
// Si el elemento está en caché, actualizamos el historial incrementando los accesos.
if ($cacheResponse and $cacheResponse->isHit() && isset($historico[$key])) {
$historico[$key]["hits_historical"] += 1; // Incrementa el total de accesos históricos.
$historico[$key]["hits"] += 1; // Incrementa los accesos en la sesión actual.
} else {
// Si no existe en el historial, añadimos una nueva entrada con los detalles de la solicitud.
$historico[$key] = [
"key" => $key,
"hits_historical" => 1,
"hits" => 0,
"endpoint" => $endpoint,
"queryParams" => $queryParams,
"global" => $global,
"expire" => $queryParams['$expire'],
"headers" => $headers ?? $this->getHeaders($global),
"time_expire" => time() + $queryParams['$expire'] // Fecha/hora de expiración calculada.
];
}
// Guardamos el historial actualizado en la caché.
$this->cache->save($cacheHistorico->set(json_encode($historico)));
}
}