PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci", ]; // TLS-in-transito (VIGILE 2026-06-10) — pre-equip, GATED, DEFAULT OFF. // Unione con '+' (NON array_merge): preserva le chiavi-intere delle // opzioni PDO (array_merge le re-indicizzerebbe). Vedi doc VIGILE §14. $options = $options + self::sslOptions(); try { self::$instance = new PDO($dsn, DB_USER, DB_PASS, $options); } catch (PDOException $e) { if (defined('APP_DEBUG') && APP_DEBUG) { throw new Exception('Database connection failed: ' . $e->getMessage()); } else { throw new Exception('Database connection failed'); } } } return self::$instance; } /** * Opzioni PDO per TLS-in-transito verso MySQL (VIGILE 2026-06-10, pre-equip). * * GATED, DEFAULT OFF: ritorna [] (nessuna modifica al comportamento attuale) * a meno che NON sia attivato esplicitamente tramite: * - env DB_SSL=true (canonico) * - OPPURE flag-file application/config/.db_ssl_on (robusto anche in * PHP-FPM dove getenv() puo' non propagare; nessun recreate necessario) * * IMPORTANTE (doc VIGILE §11): PHP PDO/mysqlnd NON cifra senza un CA file. * MYSQL_ATTR_SSL_VERIFY_SERVER_CERT=false DA SOLO connette IN CHIARO. Quindi * il TLS si attiva solo se il CA e' presente e leggibile; altrimenti si * resta in chiaro (come oggi) e si logga — MAI una connessione "a meta'". * CA host self-signed => VERIFY_SERVER_CERT=false (cifra il canale; la * verifica della CA e' hardening successivo, coordinato con VIGILE). * * @return array opzioni PDO SSL (vuoto se disattivato o CA mancante) */ private static function sslOptions(): array { $enabled = Env::bool('DB_SSL', false) || is_file(__DIR__ . '/.db_ssl_on'); if (!$enabled) { return []; } $ca = Env::get('DB_SSL_CA', __DIR__ . '/db-ca.pem'); if (!$ca || !is_file($ca) || !is_readable($ca)) { error_log('[Database] DB_SSL attivo ma CA non trovato/leggibile (' . $ca . '): connessione NON cifrata (serve il CA file).'); return []; } return [ PDO::MYSQL_ATTR_SSL_CA => $ca, PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, ]; } /** * Esegue una query con parametri */ public static function query(string $sql, array $params = []): PDOStatement { $stmt = self::getInstance()->prepare($sql); $stmt->execute($params); return $stmt; } /** * Ottiene una singola riga */ public static function fetchOne(string $sql, array $params = []): ?array { $result = self::query($sql, $params)->fetch(); return $result ?: null; } /** * Ottiene tutte le righe */ public static function fetchAll(string $sql, array $params = []): array { return self::query($sql, $params)->fetchAll(); } /** * Inserisce e restituisce l'ID */ public static function insert(string $table, array $data): int { $columns = implode(', ', array_keys($data)); $placeholders = implode(', ', array_fill(0, count($data), '?')); $sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})"; self::query($sql, array_values($data)); return (int) self::getInstance()->lastInsertId(); } /** * Aggiorna righe */ public static function update(string $table, array $data, string $where, array $whereParams = []): int { $setParts = []; foreach (array_keys($data) as $column) { $setParts[] = "{$column} = ?"; } $setClause = implode(', ', $setParts); $sql = "UPDATE {$table} SET {$setClause} WHERE {$where}"; $params = array_merge(array_values($data), $whereParams); return self::query($sql, $params)->rowCount(); } /** * Elimina righe */ public static function delete(string $table, string $where, array $params = []): int { $sql = "DELETE FROM {$table} WHERE {$where}"; return self::query($sql, $params)->rowCount(); } /** * Conta righe */ public static function count(string $table, string $where = '1=1', array $params = []): int { $sql = "SELECT COUNT(*) as cnt FROM {$table} WHERE {$where}"; $result = self::fetchOne($sql, $params); return (int) ($result['cnt'] ?? 0); } /** * Inizia una transazione */ public static function beginTransaction(): bool { return self::getInstance()->beginTransaction(); } /** * Commit transazione */ public static function commit(): bool { return self::getInstance()->commit(); } /** * Rollback transazione */ public static function rollback(): bool { return self::getInstance()->rollBack(); } /** * Restituisce l'ultimo ID inserito */ public static function lastInsertId(): int { return (int) self::getInstance()->lastInsertId(); } private function __clone() {} }