PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_PERSISTENT => true, (class_exists('Pdo\Mysql') ? \Pdo\Mysql::ATTR_INIT_COMMAND : PDO::MYSQL_ATTR_INIT_COMMAND) => "SET NAMES " . DB_CHARSET . " COLLATE utf8mb4_unicode_ci", // Deshabilitar verificación SSL — conexión interna Docker (sin certificados) PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, ]; $this->pdo = new PDO($dsn, DB_USER, DB_PASS, $options); } catch (PDOException $e) { error_log("Database connection failed: " . $e->getMessage()); throw new Exception("Error de conexión a la base de datos"); } } public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public function getConnection() { return $this->pdo; } public function query($sql, $params = []) { try { $stmt = $this->pdo->prepare($sql); if (!empty($params)) { $stmt->execute($params); } else { $stmt->execute(); } return $stmt; } catch (PDOException $e) { $errorDetail = date('[Y-m-d H:i:s] ') . "DB Query FAILED: " . $e->getMessage() . "\n SQL: " . $sql . "\n Params: " . json_encode($params) . "\n Trace: " . $e->getTraceAsString() . "\n\n"; error_log("Query failed: " . $e->getMessage() . " SQL: " . $sql . " Params: " . json_encode($params)); @file_put_contents(__DIR__ . '/../logs/app_errors.log', $errorDetail, FILE_APPEND); throw new Exception("Error en la consulta a la base de datos: " . $e->getMessage()); } } public function fetch($sql, $params = []) { try { $stmt = $this->query($sql, $params); return $stmt->fetch(); } catch (Exception $e) { error_log("Fetch failed: " . $e->getMessage()); throw $e; } } public function fetchAll($sql, $params = []) { try { $stmt = $this->query($sql, $params); return $stmt->fetchAll(); } catch (Exception $e) { error_log("FetchAll failed: " . $e->getMessage()); throw $e; } } public function insert($table, $data) { $keys = array_keys($data); $fields = implode(',', $keys); $placeholders = ':' . implode(', :', $keys); $sql = "INSERT INTO {$table} ({$fields}) VALUES ({$placeholders})"; $stmt = $this->query($sql, $data); return $this->pdo->lastInsertId(); } public function update($table, $data, $where, $whereParams = []) { $fields = []; foreach (array_keys($data) as $key) { $fields[] = "{$key} = :{$key}"; } $fieldsStr = implode(', ', $fields); // Convertir WHERE params para evitar mezcla de nombrados y posicionales $whereNamed = $where; $namedWhereParams = []; if (!empty($whereParams)) { // Detectar si es asociativo (nombrado) o secuencial (posicional) $keys = array_keys($whereParams); $isAssociative = ($keys !== range(0, count($whereParams) - 1)); if ($isAssociative) { // WHERE ya usa :nombre — renombrar para evitar colisión con SET params foreach ($whereParams as $key => $value) { $newKey = '_w_' . $key; $whereNamed = str_replace(':' . $key, ':' . $newKey, $whereNamed); $namedWhereParams[$newKey] = $value; } } else { // WHERE usa ? posicionales — convertir a nombrados $i = 0; foreach ($whereParams as $value) { $paramName = '_w' . $i; $whereNamed = preg_replace('/\?/', ':' . $paramName, $whereNamed, 1); $namedWhereParams[$paramName] = $value; $i++; } } } $sql = "UPDATE {$table} SET {$fieldsStr} WHERE {$whereNamed}"; $params = array_merge($data, $namedWhereParams); return $this->query($sql, $params); } public function delete($table, $where, $params = []) { $sql = "DELETE FROM {$table} WHERE {$where}"; return $this->query($sql, $params); } public function beginTransaction() { return $this->pdo->beginTransaction(); } public function commit() { return $this->pdo->commit(); } public function rollback() { try { return $this->pdo->rollback(); } catch (PDOException $e) { // Si no hay transacción activa, simplemente ignorar if ($e->getCode() !== 'HY000' || strpos($e->getMessage(), 'no active transaction') === false) { throw $e; } return false; } } public function execute($sql, $params = []) { $stmt = $this->query($sql, $params); return $stmt->rowCount(); } public function lastInsertId() { return $this->pdo->lastInsertId(); } // Prevenir clonación private function __clone() {} // Prevenir deserialización public function __wakeup() { throw new Exception("Cannot unserialize singleton"); } } ?>