hostname) && !empty($this->username)) { $this->connect(); if (!empty($this->database)) { $this->select_db(); } if (!empty($this->encoding)) { $this->set_character(); } } } /** * Inicia a conexão com o servidor * * @throws RuntimeException */ public function connect(): void { $this->connect = mysqli_connect( $this->hostname, $this->username, $this->password ); if (!$this->connect) { throw new RuntimeException( 'Erro ao conectar ao servidor: ' . mysqli_connect_error() ); } } /** * Encerra a conexão com o servidor */ public function disconnect(): bool { return mysqli_close($this->connect); } /** * Seleciona uma base de dados * * @throws RuntimeException */ public function select_db(): void { if (!mysqli_select_db($this->connect, $this->database)) { throw new RuntimeException( 'Erro ao selecionar a base de dados: ' . $this->connect->error ); } } /** * Define o conjunto de caracteres da conexão * * @throws RuntimeException */ public function set_character(): void { $enc = $this->encoding; $sql = "SET character_set_results = '{$enc}', NAMES '{$enc}', character_set_connection = '{$enc}', character_set_database = '{$enc}', character_set_server = '{$enc}';"; if (!mysqli_query($this->connect, $sql)) { throw new RuntimeException( 'Erro ao definir charset: ' . mysqli_error($this->connect) ); } } /** * Verifica conexão ativa e executa uma query * Registra no histórico com timestamp e duração * * @param string $value SQL a executar, ou 'LAST' para repetir a última * @return $this * @throws RuntimeException */ public function query(string $value): static { if (!isset($this->connect) || !mysqli_ping($this->connect)) { $this->__construct(); } $sql = ($value === 'LAST') ? $this->last : $value; $start = microtime(true); $result = mysqli_query($this->connect, $sql); $duration = round((microtime(true) - $start) * 1000, 3); if ($result === false) { throw new RuntimeException( 'Erro ao efetuar a consulta: ' . mysqli_error($this->connect) . ' | SQL: ' . $sql ); } $this->query = $result; $this->last = $sql; $this->history[] = [ 'time' => date('Y-m-d H:i:s'), 'query' => $sql, 'duration' => $duration . 'ms', ]; $this->profile[] = $duration; return $this; } /** * Executa uma query com prepared statement (proteção contra SQL injection) * * @param string $query SQL com placeholders '?' * @param array $params Valores a substituir nos placeholders * @param string $types Tipos dos parâmetros: s=string, i=int, d=double, b=blob * (detectado automaticamente se omitido) * @return $this * @throws RuntimeException * * @example * $db->prepare("SELECT * FROM users WHERE email = ? AND active = ?", ['user@mail.com', 1], 'si'); */ public function prepare(string $query, array $params = [], string $types = ''): static { if (!isset($this->connect) || !mysqli_ping($this->connect)) { $this->__construct(); } $stmt = mysqli_prepare($this->connect, $query); if (!$stmt) { throw new RuntimeException( 'Erro ao preparar statement: ' . mysqli_error($this->connect) ); } if (!empty($params)) { if (empty($types)) { $types = implode('', array_map(function ($v) { return is_int($v) ? 'i' : (is_float($v) ? 'd' : 's'); }, $params)); } mysqli_stmt_bind_param($stmt, $types, ...$params); } $start = microtime(true); mysqli_stmt_execute($stmt); $duration = round((microtime(true) - $start) * 1000, 3); $this->query = mysqli_stmt_get_result($stmt); $this->last = $query; $this->history[] = [ 'time' => date('Y-m-d H:i:s'), 'query' => $query, 'params' => $params, 'duration' => $duration . 'ms', ]; $this->profile[] = $duration; return $this; } /** * Executa uma query com cache em memória * Reutiliza o resultado se a mesma query já foi executada nesta requisição * * @param string $query SQL a executar * @param int|null $ttl Tempo de vida em segundos (null = sem expiração) * @return $this * * @example * $db->query_cache("SELECT * FROM config", 60)->fetch_assoc(); */ public function query_cache(string $query, ?int $ttl = null): static { $key = md5($query); $now = time(); if (isset($this->cache[$key])) { $entry = $this->cache[$key]; if ($ttl === null || ($now - $entry['time']) < $ttl) { $this->query = $entry['result']; return $this; } } $this->query($query); // Armazena uma cópia dos dados (não o resource, que é single-read) $rows = []; if ($this->query && is_object($this->query)) { while ($row = $this->query->fetch_array(MYSQLI_ASSOC)) { $rows[] = $row; } $this->cache[$key] = ['result' => $rows, 'time' => $now]; // Cria um "resultado virtual" iterável via array $this->query = $rows; } return $this; } /** * Limpa todo o cache de queries ou uma entrada específica * * @param string|null $query SQL específico para limpar (null = limpa tudo) */ public function clear_cache(?string $query = null): void { if ($query === null) { $this->cache = []; } else { unset($this->cache[md5($query)]); } } // ------------------------------------------------------------------------- // INSERT / UPDATE / DELETE // ------------------------------------------------------------------------- /** * Insere um registro usando array associativo * Usa escape em todos os valores * * @param string $table Nome da tabela * @param array $array Dados no formato ['coluna' => 'valor'] * @return $this * * @example * $db->insert_array('users', ['name' => 'Ana', 'email' => 'ana@mail.com']); */ public function insert_array(string $table, array $array): static { foreach ($array as $key => $value) { $columns[] = '`' . $key . '`'; $fields[] = ($value !== '' && $value !== null) ? "'" . $this->escape((string) $value) . "'" : 'NULL'; } $columns = implode(', ', $columns); $fields = implode(', ', $fields); return $this->query("INSERT INTO `{$table}` ({$columns}) VALUES ({$fields});"); } /** * Atualiza registros usando array associativo * Usa escape em todos os valores * * @param string $table Nome da tabela * @param array $array Dados no formato ['coluna' => 'valor'] * @param string $where Condição WHERE (sem a palavra WHERE) * @return $this * @throws InvalidArgumentException * * @example * $db->update_array('users', ['name' => 'João'], "id = 5"); */ public function update_array(string $table, array $array, string $where): static { if (empty($where)) { throw new InvalidArgumentException( 'update_array() requer uma condição WHERE para evitar atualização de todos os registros.' ); } foreach ($array as $key => $value) { $set[] = '`' . $key . '` = ' . ( ($value !== '' && $value !== null) ? "'" . $this->escape((string) $value) . "'" : 'NULL' ); } $set = implode(', ', $set); return $this->query("UPDATE `{$table}` SET {$set} WHERE {$where};"); } /** * Exclui registros de uma tabela com condição obrigatória * * @param string $table Nome da tabela * @param string $where Condição WHERE (sem a palavra WHERE) * @return $this * @throws InvalidArgumentException * * @example * $db->delete_where('users', "id = 10"); */ public function delete_where(string $table, string $where): static { if (empty($where)) { throw new InvalidArgumentException( 'delete_where() requer uma condição WHERE para evitar exclusão de todos os registros.' ); } return $this->query("DELETE FROM `{$table}` WHERE {$where};"); } // ------------------------------------------------------------------------- // FETCH // ------------------------------------------------------------------------- /** * Retorna todos os resultados como array numérico e associativo */ public function fetch_array(): array { // Suporte a resultado em cache (array nativo) if (is_array($this->query)) { return $this->query; } while ($array[] = $this->query->fetch_array(MYSQLI_BOTH)); return array_filter($array); } /** * Retorna todos os resultados como array associativo */ public function fetch_assoc(): array { if (is_array($this->query)) { return $this->query; } while ($array[] = $this->query->fetch_array(MYSQLI_ASSOC)); return array_filter($array); } /** * Retorna todos os resultados como array numérico */ public function fetch_row(): array { if (is_array($this->query)) { return array_values($this->query); } while ($array[] = $this->query->fetch_array(MYSQLI_NUM)); return array_filter($array); } /** * Retorna apenas o primeiro resultado como array associativo (ou null) * * @example * $user = $db->query("SELECT * FROM users WHERE id = 1")->fetch_one(); */ public function fetch_one(): ?array { if (is_array($this->query)) { return $this->query[0] ?? null; } return $this->query->fetch_array(MYSQLI_ASSOC) ?: null; } /** * Retorna o valor de uma única coluna do primeiro resultado * * @param string|int $column Nome ou índice da coluna (padrão: primeira coluna) * @return mixed * * @example * $total = $db->query("SELECT COUNT(*) FROM users")->fetch_value(); * $email = $db->query("SELECT email FROM users WHERE id = 1")->fetch_value('email'); */ public function fetch_value(string|int $column = 0): mixed { if (is_array($this->query)) { $row = $this->query[0] ?? null; return $row ? (is_int($column) ? array_values($row)[$column] : $row[$column]) : null; } $row = $this->query->fetch_array(MYSQLI_BOTH); return $row[$column] ?? null; } /** * Retorna os nomes das colunas do resultado */ public function fetch_field(): array { while ($field = $this->query->fetch_field()) { $array[] = $field->name; } return $array ?? []; } // ------------------------------------------------------------------------- // PAGINAÇÃO // ------------------------------------------------------------------------- /** * Executa uma query com paginação automática (LIMIT + OFFSET) * * @param string $query SQL sem LIMIT/OFFSET * @param int $page Número da página (começa em 1) * @param int $per_page Registros por página * @return $this * * @example * $db->paginate("SELECT * FROM products ORDER BY name", 2, 10)->fetch_assoc(); */ public function paginate(string $query, int $page = 1, int $per_page = 10): static { $page = max(1, $page); $offset = ($page - 1) * $per_page; return $this->query($query . " LIMIT {$per_page} OFFSET {$offset}"); } /** * Conta o total de registros de uma tabela com condição opcional * * @param string $table Nome da tabela * @param string $where Condição WHERE opcional (padrão: todos os registros) * @return int * * @example * $total = $db->count('users'); * $ativos = $db->count('users', "active = 1"); */ public function count(string $table, string $where = '1'): int { return (int) $this->query( "SELECT COUNT(*) FROM `{$table}` WHERE {$where}" )->fetch_value(); } // ------------------------------------------------------------------------- // TRANSAÇÕES // ------------------------------------------------------------------------- /** * Inicia uma transação * * @return $this * * @example * $db->begin_transaction(); * try { * $db->insert_array('accounts', [...]); * $db->update_array('balances', [...], "id = 1"); * $db->commit(); * } catch (Exception $e) { * $db->rollback(); * } */ public function begin_transaction(): static { mysqli_autocommit($this->connect, false); mysqli_begin_transaction($this->connect); $this->in_transaction = true; return $this; } /** * Confirma a transação atual * * @return $this * @throws RuntimeException */ public function commit(): static { if (!$this->in_transaction) { throw new RuntimeException('Nenhuma transação ativa para confirmar.'); } mysqli_commit($this->connect); mysqli_autocommit($this->connect, true); $this->in_transaction = false; return $this; } /** * Desfaz a transação atual * * @return $this */ public function rollback(): static { if ($this->in_transaction) { mysqli_rollback($this->connect); mysqli_autocommit($this->connect, true); $this->in_transaction = false; } return $this; } /** * Verifica se há uma transação ativa */ public function is_in_transaction(): bool { return $this->in_transaction; } // ------------------------------------------------------------------------- // SEGURANÇA E UTILITÁRIOS // ------------------------------------------------------------------------- /** * Escapa uma string para uso seguro em queries * * @param string $value Valor a escapar * @return string * * @example * $nome = $db->escape($_POST['nome']); * $db->query("SELECT * FROM users WHERE name = '{$nome}'"); */ public function escape(string $value): string { return mysqli_real_escape_string($this->connect, $value); } /** * Executa EXPLAIN na última query e retorna o plano de execução * Útil para identificar queries lentas e falta de índices * * @return array * * @example * $db->query("SELECT * FROM orders WHERE user_id = 1"); * $plan = $db->explain(); */ public function explain(): array { if (empty($this->last)) { return []; } return $this->query("EXPLAIN {$this->last}")->fetch_assoc(); } // ------------------------------------------------------------------------- // INFORMAÇÕES DO RESULTADO // ------------------------------------------------------------------------- /** Número de linhas retornadas */ public function num_rows(): int { if (is_array($this->query)) { return count($this->query); } return mysqli_num_rows($this->query); } /** Número de colunas no resultado */ public function num_fields(): int { return mysqli_field_count($this->connect); } /** Número de linhas afetadas pela última operação */ public function affected_rows(): int { return mysqli_affected_rows($this->connect); } /** ID gerado pelo último INSERT */ public function insert_id(): int { return mysqli_insert_id($this->connect); } // ------------------------------------------------------------------------- // HISTÓRICO E PERFORMANCE // ------------------------------------------------------------------------- /** * Retorna o histórico completo de queries executadas */ public function get_history(): array { return $this->history; } /** * Retorna métricas de performance das queries * Inclui total de queries, tempo total e tempo médio * * @return array * * @example * $metrics = $db->get_profile(); * echo "Queries: {$metrics['total']} | Tempo total: {$metrics['total_ms']}ms"; */ public function get_profile(): array { $total = count($this->profile); $sum = array_sum($this->profile); return [ 'total' => $total, 'total_ms' => round($sum, 3), 'avg_ms' => $total > 0 ? round($sum / $total, 3) : 0, 'slowest' => $total > 0 ? max($this->profile) : 0, 'queries' => $this->profile, ]; } // ------------------------------------------------------------------------- // MEMÓRIA // ------------------------------------------------------------------------- /** * Libera o resultado atual da memória */ public function free_result(): bool { if (is_object($this->query)) { return mysqli_free_result($this->query); } return false; } }