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'; } }