403 lines
13 KiB
PHP
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';
|
|
}
|
|
}
|