<?php
/**
* Storage - Administração da base de dados com MySQLi
* Última modificação: 2026-03-13
*/
class Storage {
/**
* Configurações de conexão
*/
public
$hostname = '',
$username = '',
$password = '',
$database = '',
$encoding = 'utf8mb4';
/**
* Histórico, cache e estado interno
*/
private
$connect,
$query,
$last,
$history = [],
$cache = [],
$in_transaction = false,
$profile = [];
// -------------------------------------------------------------------------
// CONEXÃO
// -------------------------------------------------------------------------
/**
* Conexão inteligente: conecta, seleciona banco e define charset automaticamente
*/
public function __construct() {
if (!empty($this->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 = ?", ['[email protected]', 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' => '[email protected]']);
*/
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;
}
}
Comments