Huymada icon

Storage

Huymada | PRO | 03/13/26 03:22:13 PM UTC (Edited) | 0 ⭐ | 366 👁️ | Never ⏰ | []
PHP |

19.46 KB

|

None

|

0 👍

/

0 👎

<?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