Files
zibo-dashboard/public/userarea/class/MagoClient.php
T
2026-10-13 14:21:39 +02:00

403 lines
13 KiB
PHP

<?php
/**
* MagoClient - client REST in SOLA LETTURA per Mago4 (Zucchetti) - ERPServiceProvider
*
* Uso tipico:
* $mago = MagoClient::fromEnv();
* foreach ($mago->iterate('MA_SaleOrd', ['filters' => [...]]) as $rec) { ... }
* $mago->logoff(); // SEMPRE: le sessioni occupano licenza
*
* Variabili .env richieste:
* MAGO_BASE_URL, MAGO_USER, MAGO_PASSWORD, MAGO_COMPANY
* Opzionale:
* MAGO_TIMEOUT (secondi, default 60)
*
* Compatibile PHP 7.4+
*/
class MagoApiException extends RuntimeException {}
class MagoClient
{
/** @var string */
private $baseUrl;
/** @var string */
private $username;
/** @var string */
private $password;
/** @var string */
private $company;
/** @var int */
private $timeout;
/** @var string|null */
private $token = null;
/** @var string|null */
private $lastLogoffError = null;
/** @var string|null percorso di logoff che ha risposto correttamente */
private $logoffPath = null;
public function __construct($baseUrl, $username, $password, $company, $timeout = 60)
{
$this->baseUrl = rtrim((string)$baseUrl, '/') . '/';
$this->username = (string)$username;
$this->password = (string)$password;
$this->company = (string)$company;
$this->timeout = max(5, (int)$timeout);
}
/**
* Crea il client leggendo le credenziali da .env (dotenv) / variabili d'ambiente.
*/
public static function fromEnv()
{
$get = function ($k, $default = null) {
if (isset($_ENV[$k]) && $_ENV[$k] !== '') return $_ENV[$k];
if (isset($_SERVER[$k]) && $_SERVER[$k] !== '') return $_SERVER[$k];
$v = getenv($k);
return ($v !== false && $v !== '') ? $v : $default;
};
$cfg = [
'MAGO_BASE_URL' => $get('MAGO_BASE_URL'),
'MAGO_USER' => $get('MAGO_USER'),
'MAGO_PASSWORD' => $get('MAGO_PASSWORD'),
'MAGO_COMPANY' => $get('MAGO_COMPANY'),
];
$missing = array_keys(array_filter($cfg, function ($v) {
return $v === null;
}));
if ($missing) {
throw new MagoApiException('Variabili .env mancanti: ' . implode(', ', $missing));
}
return new self(
$cfg['MAGO_BASE_URL'],
$cfg['MAGO_USER'],
$cfg['MAGO_PASSWORD'],
$cfg['MAGO_COMPANY'],
(int)$get('MAGO_TIMEOUT', 60)
);
}
public function __destruct()
{
// Rete di sicurezza: se lo script dimentica il logoff, libero comunque la licenza
$this->logoff();
}
public function isLoggedIn()
{
return $this->token !== null;
}
public function getLastLogoffError()
{
return $this->lastLogoffError;
}
public function getLogoffPath()
{
return $this->logoffPath;
}
/* =========================================================
* SESSIONE
* ========================================================= */
public function login()
{
$r = $this->request('api/account-manager/login', [
'username' => $this->username,
'password' => $this->password,
'company' => $this->company,
], false);
$token = $r['data']['token'] ?? null;
if (!is_string($token) || $token === '') {
throw new MagoApiException('Login Mago fallito (HTTP ' . $r['status'] . '): ' . $this->extractMessage($r));
}
$this->token = $token;
return $this->token;
}
/**
* Chiude la sessione e libera lo slot di licenza.
* Non lancia eccezioni: ritorna true/false (dettaglio in getLastLogoffError()).
*/
public function logoff()
{
if ($this->token === null) return true;
$this->lastLogoffError = null;
// Il percorso "logoff" ha risposto 404: provo le varianti note e memorizzo quella buona
$candidates = $this->logoffPath
? [$this->logoffPath]
: ['api/account-manager/logoff', 'api/account-manager/logout', 'api/account-manager/logOff'];
$errors = [];
try {
foreach ($candidates as $path) {
$r = $this->request($path, ['token' => $this->token], true);
if ($r['status'] >= 400) {
$errors[] = $path . ' → HTTP ' . $r['status'];
continue;
}
$this->logoffPath = $path;
return true;
}
$this->lastLogoffError = implode(' | ', $errors);
return false;
} catch (Throwable $e) {
$this->lastLogoffError = $e->getMessage();
return false;
} finally {
$this->token = null;
}
}
/* =========================================================
* LETTURA DATI
* ========================================================= */
/**
* Legge UNA pagina di una tabella.
*
* $opts:
* 'fields' => ['Campo1', 'Campo2'] (opzionale)
* 'filters' => [['key'=>'OrderDate','value'=>'2026-01-01T00:00:00','operator'=>'>=']]
* value può essere anche DateTimeInterface
* 'recordsPerPage' => 200 (massimo accettato da Mago)
* 'pageNumber' => 1
*
* Ritorna: ['total'=>int, 'count'=>int, 'page'=>int, 'records'=>array]
*/
public function getDataTable($tableName, array $opts = [])
{
$body = [
'tableName' => (string)$tableName,
'recordsPerPage' => max(1, min(200, (int)($opts['recordsPerPage'] ?? 200))), // Mago restituisce max 200 per pagina
'pageNumber' => max(1, (int)($opts['pageNumber'] ?? 1)),
];
if (!empty($opts['fields'])) {
$body['fieldsName'] = array_values($opts['fields']);
}
if (!empty($opts['filters'])) {
$body['filters'] = $this->normalizeFilters($opts['filters']);
}
$data = $this->callWithRelogin('api/magoservices/getDataTable', $body);
if (($data['response'] ?? '') !== 'OK') {
throw new MagoApiException("getDataTable($tableName) risposta non OK: " . $this->extractMessage(['data' => $data, 'raw' => json_encode($data)]));
}
$d = $data['responseData'] ?? [];
return [
'total' => (int)($d['totalRecordsNumber'] ?? 0),
'count' => (int)($d['currentPageRecordsNumber'] ?? 0),
'page' => (int)($d['currentPageNumber'] ?? $body['pageNumber']),
'records' => is_array($d['records'] ?? null) ? $d['records'] : [],
];
}
/**
* Itera TUTTI i record di una tabella paginando in automatico (generator, poca memoria).
*
* foreach ($mago->iterate('MA_SaleOrd', ['filters'=>[...], 'recordsPerPage'=>500]) as $rec) {...}
*/
public function iterate($tableName, array $opts = [], $maxPages = 100000)
{
$perPage = max(1, min(200, (int)($opts['recordsPerPage'] ?? 200)));
$page = 1;
$read = 0;
do {
$r = $this->getDataTable($tableName, array_merge($opts, [
'recordsPerPage' => $perPage,
'pageNumber' => $page,
]));
if (empty($r['records'])) break;
foreach ($r['records'] as $rec) {
$read++;
yield $rec;
}
$page++;
} while ($read < $r['total'] && $page <= $maxPages);
}
/**
* Conta i record (chiede 1 solo record e legge totalRecordsNumber).
*/
public function count($tableName, array $filters = [])
{
$r = $this->getDataTable($tableName, [
'filters' => $filters,
'recordsPerPage' => 1,
'pageNumber' => 1,
]);
return $r['total'];
}
/* =========================================================
* HELPER DI CONVERSIONE (per l'import in MySQL)
* ========================================================= */
/**
* Converte una data Mago in formato MySQL. Le date "vuote" di Mago (1799-12-31) diventano null.
*/
public static function toMysqlDate($value, $withTime = false)
{
if ($value === null) return null;
$value = trim((string)$value);
if ($value === '' || strpos($value, '1799-12-31') === 0) return null;
$ts = strtotime($value);
if ($ts === false) return null;
return date($withTime ? 'Y-m-d H:i:s' : 'Y-m-d', $ts);
}
/**
* Converte i booleani di Mago (true/false, 1/0, "1"/"0", "true"/"false", "Y"/"N") in 0/1.
*/
public static function toBool($value)
{
if (is_bool($value)) return $value ? 1 : 0;
$v = strtolower(trim((string)$value));
return in_array($v, ['1', 'true', 'y', 'yes', 's', 'si'], true) ? 1 : 0;
}
public static function toDecimal($value)
{
if ($value === null || $value === '') return 0.0;
return (float)str_replace(',', '.', (string)$value);
}
/* =========================================================
* INTERNI
* ========================================================= */
private function normalizeFilters(array $filters)
{
$out = [];
foreach ($filters as $f) {
if (!isset($f['key'])) continue;
$value = $f['value'] ?? '';
if ($value instanceof DateTimeInterface) {
$value = $value->format('Y-m-d\TH:i:s');
}
// Verificato: una data SENZA orario dà risultati errati → aggiungo sempre T00:00:00
if (is_string($value) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
$value .= 'T00:00:00';
}
$out[] = [
'key' => (string)$f['key'],
'value' => is_bool($value) ? ($value ? 'true' : 'false') : (string)$value,
'operator' => (string)($f['operator'] ?? '='),
];
}
return $out;
}
private function callWithRelogin($path, $body)
{
if ($this->token === null) {
$this->login();
}
$r = $this->request($path, $body, true);
// Token scaduto → rifaccio login UNA volta e ritento
if ($this->isAuthError($r)) {
$this->token = null;
$this->login();
$r = $this->request($path, $body, true);
}
if ($r['status'] >= 400 || $r['data'] === null) {
throw new MagoApiException('Errore Mago (HTTP ' . $r['status'] . ') su ' . $path . ': ' . $this->extractMessage($r));
}
return $r['data'];
}
private function isAuthError(array $r)
{
$raw = (string)$r['raw'];
return stripos($raw, 'LoginInfo Not Found') !== false
|| stripos($raw, 'Missing token') !== false
|| $r['status'] === 401;
}
private function request($path, $body, $auth)
{
if (!function_exists('curl_init')) {
throw new MagoApiException('Estensione PHP cURL non disponibile');
}
$headers = [
'Content-Type: application/json',
'Accept: application/json',
];
if ($auth) {
if ($this->token === null) {
throw new MagoApiException('Chiamata autenticata senza token (login non eseguito)');
}
$headers[] = 'Authorization: ' . $this->token; // token nudo, senza "Bearer"
}
$ch = curl_init($this->baseUrl . ltrim($path, '/'));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => $this->timeout,
]);
$raw = curl_exec($ch);
if ($raw === false) {
$err = curl_error($ch);
curl_close($ch);
throw new MagoApiException('Connessione a Mago fallita (VPN attiva?): ' . $err);
}
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true);
return [
'status' => $status,
'data' => is_array($data) ? $data : null,
'raw' => $raw,
];
}
private function extractMessage(array $r)
{
$d = $r['data'] ?? null;
if (is_array($d)) {
foreach (['message', 'Message', 'error', 'errorMessage', 'responseMessage', 'response'] as $k) {
if (!empty($d[$k]) && is_string($d[$k])) return $d[$k];
}
}
$raw = trim((string)($r['raw'] ?? ''));
return $raw !== '' ? mb_substr($raw, 0, 300) : 'risposta vuota';
}
}