/home/suroeste/public_html/payments.transportessuroeste.com/src/Models
Edit: /home/suroeste/public_html/payments.transportessuroeste.com/src/Models/Database.php (8086B)
connect();
}
public static function getInstance(): Database
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function connect(): void
{
try {
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
DB_CONFIG['host'],
DB_CONFIG['port'],
DB_CONFIG['database'],
DB_CONFIG['charset']
);
$this->pdo = new PDO($dsn, DB_CONFIG['username'], DB_CONFIG['password'], DB_CONFIG['options']);
$this->pdo->exec("SET SESSION sql_mode = 'STRICT_ALL_TABLES'");
} catch (PDOException $e) {
error_log("DB Connection Error: " . $e->getMessage());
throw new DatabaseException('No se pudo conectar a la base de datos', 503, $e, (string)($e->errorInfo[1] ?? ''));
}
}
/**
* Obtener conexión PDO directa
*/
public function getPdo(): PDO
{
return $this->pdo;
}
/**
* Alias de getPdo para compatibilidad
*/
public function getConnection(): PDO
{
return $this->pdo;
}
/**
* Ejecutar consulta preparada (SEGURA contra SQL Injection)
*/
public function query(string $sql, array $params = []): \PDOStatement
{
try {
$stmt = $this->pdo->prepare($sql);
if (empty($params)) {
$stmt->execute();
return $stmt;
}
// Detectar si es array asociativo o indexado
$isAssoc = array_keys($params) !== range(0, count($params) - 1);
if ($isAssoc) {
// Named parameters
foreach ($params as $key => $value) {
$paramName = ':' . ltrim($key, ':');
$type = $this->getPdoType($value);
$stmt->bindValue($paramName, $value, $type);
}
$stmt->execute();
} else {
// Positional parameters
$stmt->execute($params);
}
return $stmt;
} catch (PDOException $e) {
// Log completo para debug (solo en servidor, nunca al usuario)
error_log(sprintf(
"SQL Error [%s]: %s | SQL: %s | Params: %s",
$e->getCode(),
$e->getMessage(),
$sql,
json_encode($params)
));
throw $this->translatePdoException($e);
}
}
/**
* Obtener tipo PDO para un valor
*/
private function getPdoType($value): int
{
return match (true) {
is_int($value) => PDO::PARAM_INT,
is_bool($value) => PDO::PARAM_BOOL,
is_null($value) => PDO::PARAM_NULL,
default => PDO::PARAM_STR
};
}
/**
* Obtener un solo registro
*/
public function fetchOne(string $sql, array $params = []): ?array
{
$result = $this->query($sql, $params)->fetch(PDO::FETCH_ASSOC);
return $result !== false ? $result : null;
}
/**
* Obtener todos los registros
*/
public function fetchAll(string $sql, array $params = []): array
{
return $this->query($sql, $params)->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Insertar registro
*/
public function insert(string $table, array $data): int
{
$columns = array_keys($data);
$placeholders = array_map(fn($c) => ':' . $c, $columns);
$sql = sprintf(
'INSERT INTO `%s` (`%s`) VALUES (%s)',
$table,
implode('`, `', $columns),
implode(', ', $placeholders)
);
$this->query($sql, $data);
return (int) $this->pdo->lastInsertId();
}
/**
* Actualizar registros
*/
public function update(string $table, array $data, string $where, array $whereParams = []): int
{
$set = [];
foreach (array_keys($data) as $col) {
$set[] = "`{$col}` = :set_{$col}";
}
$sql = sprintf('UPDATE `%s` SET %s WHERE %s', $table, implode(', ', $set), $where);
$params = [];
foreach ($data as $k => $v) {
$params['set_' . $k] = $v;
}
return $this->query($sql, array_merge($params, $whereParams))->rowCount();
}
/**
* Eliminar registros
*/
public function delete(string $table, string $where, array $params = []): int
{
$sql = sprintf('DELETE FROM `%s` WHERE %s', $table, $where);
return $this->query($sql, $params)->rowCount();
}
/**
* Iniciar transacción
*/
public function beginTransaction(): bool
{
if ($this->transactionLevel === 0) {
$this->pdo->beginTransaction();
} else {
$this->pdo->exec("SAVEPOINT level_{$this->transactionLevel}");
}
$this->transactionLevel++;
return true;
}
/**
* Confirmar transacción
*/
public function commit(): bool
{
$this->transactionLevel--;
if ($this->transactionLevel === 0) {
return $this->pdo->commit();
}
return true;
}
/**
* Revertir transacción
*/
public function rollback(): bool
{
$this->transactionLevel--;
if ($this->transactionLevel === 0) {
return $this->pdo->rollBack();
}
$this->pdo->exec("ROLLBACK TO SAVEPOINT level_{$this->transactionLevel}");
return true;
}
/**
* Obtener último ID insertado
*/
public function lastInsertId(): int
{
return (int) $this->pdo->lastInsertId();
}
/**
* Traducir PDOException a DatabaseException con mensaje seguro
*/
private function translatePdoException(PDOException $e): DatabaseException
{
$errorInfo = $e->errorInfo ?? [];
$mysqlCode = (string)($errorInfo[1] ?? '');
$constraintName = '';
// Extraer nombre de constraint si existe
if (preg_match("/for key '([^']+)'/", $e->getMessage(), $matches)) {
$constraintName = $matches[1];
}
$safeMessage = match($mysqlCode) {
'1062' => 'Ya existe un registro con estos datos',
'1452' => 'Referencia a registro inexistente',
'1451' => 'No se puede eliminar: existen registros relacionados',
'1048' => 'Faltan campos obligatorios',
'1406' => 'Uno de los campos excede la longitud permitida',
'1264' => 'Valor fuera de rango permitido',
'1366' => 'Valor incorrecto para el tipo de campo',
'2002', '2003', '2006' => 'Error de conexion con la base de datos',
'1213' => 'Conflicto de concurrencia, intente de nuevo',
'1205' => 'Tiempo de espera agotado, intente de nuevo',
default => 'Error interno de base de datos'
};
$httpCode = match($mysqlCode) {
'1062' => 409,
'1452', '1451' => 422,
'1048', '1406', '1264', '1366' => 400,
'2002', '2003', '2006' => 503,
'1213', '1205' => 503,
default => 500
};
return new DatabaseException($safeMessage, $httpCode, $e, $mysqlCode, $constraintName);
}
private function __clone() {}
public function __wakeup() { throw new DatabaseException('No serializable'); }
}