Huymada

Storage

Apr 27th, 2017 (edited)
476
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 19.46 KB | None | 0 0
  1. <?php
  2. /**
  3.  * Storage - Administração da base de dados com MySQLi
  4.  * Última modificação: 2026-03-13
  5.  */
  6. class Storage {
  7.  
  8.     /**
  9.      * Configurações de conexão
  10.      */
  11.     public
  12.         $hostname = '',
  13.         $username = '',
  14.         $password = '',
  15.         $database = '',
  16.         $encoding = 'utf8mb4';
  17.  
  18.     /**
  19.      * Histórico, cache e estado interno
  20.      */
  21.     private
  22.         $connect,
  23.         $query,
  24.         $last,
  25.         $history         = [],
  26.         $cache           = [],
  27.         $in_transaction  = false,
  28.         $profile         = [];
  29.  
  30.     // -------------------------------------------------------------------------
  31.     // CONEXÃO
  32.     // -------------------------------------------------------------------------
  33.  
  34.     /**
  35.      * Conexão inteligente: conecta, seleciona banco e define charset automaticamente
  36.      */
  37.     public function __construct() {
  38.         if (!empty($this->hostname) && !empty($this->username)) {
  39.             $this->connect();
  40.  
  41.             if (!empty($this->database)) {
  42.                 $this->select_db();
  43.             }
  44.  
  45.             if (!empty($this->encoding)) {
  46.                 $this->set_character();
  47.             }
  48.         }
  49.     }
  50.  
  51.     /**
  52.      * Inicia a conexão com o servidor
  53.      *
  54.      * @throws RuntimeException
  55.      */
  56.     public function connect(): void {
  57.         $this->connect = mysqli_connect(
  58.             $this->hostname,
  59.             $this->username,
  60.             $this->password
  61.         );
  62.  
  63.         if (!$this->connect) {
  64.             throw new RuntimeException(
  65.                 'Erro ao conectar ao servidor: ' . mysqli_connect_error()
  66.             );
  67.         }
  68.     }
  69.  
  70.     /**
  71.      * Encerra a conexão com o servidor
  72.      */
  73.     public function disconnect(): bool {
  74.         return mysqli_close($this->connect);
  75.     }
  76.  
  77.     /**
  78.      * Seleciona uma base de dados
  79.      *
  80.      * @throws RuntimeException
  81.      */
  82.     public function select_db(): void {
  83.         if (!mysqli_select_db($this->connect, $this->database)) {
  84.             throw new RuntimeException(
  85.                 'Erro ao selecionar a base de dados: ' . $this->connect->error
  86.             );
  87.         }
  88.     }
  89.  
  90.     /**
  91.      * Define o conjunto de caracteres da conexão
  92.      *
  93.      * @throws RuntimeException
  94.      */
  95.     public function set_character(): void {
  96.         $enc = $this->encoding;
  97.         $sql = "SET character_set_results = '{$enc}', NAMES '{$enc}',
  98.                character_set_connection = '{$enc}',
  99.                character_set_database = '{$enc}',
  100.                character_set_server = '{$enc}';";
  101.  
  102.         if (!mysqli_query($this->connect, $sql)) {
  103.             throw new RuntimeException(
  104.                 'Erro ao definir charset: ' . mysqli_error($this->connect)
  105.             );
  106.         }
  107.     }
  108.  
  109.     /**
  110.      * Verifica conexão ativa e executa uma query
  111.      * Registra no histórico com timestamp e duração
  112.      *
  113.      * @param  string $value  SQL a executar, ou 'LAST' para repetir a última
  114.      * @return $this
  115.      * @throws RuntimeException
  116.      */
  117.     public function query(string $value): static {
  118.         if (!isset($this->connect) || !mysqli_ping($this->connect)) {
  119.             $this->__construct();
  120.         }
  121.  
  122.         $sql   = ($value === 'LAST') ? $this->last : $value;
  123.         $start = microtime(true);
  124.  
  125.         $result = mysqli_query($this->connect, $sql);
  126.  
  127.         $duration = round((microtime(true) - $start) * 1000, 3);
  128.  
  129.         if ($result === false) {
  130.             throw new RuntimeException(
  131.                 'Erro ao efetuar a consulta: ' . mysqli_error($this->connect)
  132.                 . ' | SQL: ' . $sql
  133.             );
  134.         }
  135.  
  136.         $this->query   = $result;
  137.         $this->last    = $sql;
  138.         $this->history[] = [
  139.             'time'     => date('Y-m-d H:i:s'),
  140.             'query'    => $sql,
  141.             'duration' => $duration . 'ms',
  142.         ];
  143.         $this->profile[] = $duration;
  144.  
  145.         return $this;
  146.     }
  147.  
  148.     /**
  149.      * Executa uma query com prepared statement (proteção contra SQL injection)
  150.      *
  151.      * @param  string $query   SQL com placeholders '?'
  152.      * @param  array  $params  Valores a substituir nos placeholders
  153.      * @param  string $types   Tipos dos parâmetros: s=string, i=int, d=double, b=blob
  154.      *                         (detectado automaticamente se omitido)
  155.      * @return $this
  156.      * @throws RuntimeException
  157.      *
  158.      * @example
  159.      *   $db->prepare("SELECT * FROM users WHERE email = ? AND active = ?", ['[email protected]', 1], 'si');
  160.      */
  161.     public function prepare(string $query, array $params = [], string $types = ''): static {
  162.         if (!isset($this->connect) || !mysqli_ping($this->connect)) {
  163.             $this->__construct();
  164.         }
  165.  
  166.         $stmt = mysqli_prepare($this->connect, $query);
  167.  
  168.         if (!$stmt) {
  169.             throw new RuntimeException(
  170.                 'Erro ao preparar statement: ' . mysqli_error($this->connect)
  171.             );
  172.         }
  173.  
  174.         if (!empty($params)) {
  175.             if (empty($types)) {
  176.                 $types = implode('', array_map(function ($v) {
  177.                     return is_int($v) ? 'i' : (is_float($v) ? 'd' : 's');
  178.                 }, $params));
  179.             }
  180.             mysqli_stmt_bind_param($stmt, $types, ...$params);
  181.         }
  182.  
  183.         $start = microtime(true);
  184.         mysqli_stmt_execute($stmt);
  185.         $duration = round((microtime(true) - $start) * 1000, 3);
  186.  
  187.         $this->query   = mysqli_stmt_get_result($stmt);
  188.         $this->last    = $query;
  189.         $this->history[] = [
  190.             'time'     => date('Y-m-d H:i:s'),
  191.             'query'    => $query,
  192.             'params'   => $params,
  193.             'duration' => $duration . 'ms',
  194.         ];
  195.         $this->profile[] = $duration;
  196.  
  197.         return $this;
  198.     }
  199.  
  200.     /**
  201.      * Executa uma query com cache em memória
  202.      * Reutiliza o resultado se a mesma query já foi executada nesta requisição
  203.      *
  204.      * @param  string   $query  SQL a executar
  205.      * @param  int|null $ttl    Tempo de vida em segundos (null = sem expiração)
  206.      * @return $this
  207.      *
  208.      * @example
  209.      *   $db->query_cache("SELECT * FROM config", 60)->fetch_assoc();
  210.      */
  211.     public function query_cache(string $query, ?int $ttl = null): static {
  212.         $key = md5($query);
  213.         $now = time();
  214.  
  215.         if (isset($this->cache[$key])) {
  216.             $entry = $this->cache[$key];
  217.             if ($ttl === null || ($now - $entry['time']) < $ttl) {
  218.                 $this->query = $entry['result'];
  219.                 return $this;
  220.             }
  221.         }
  222.  
  223.         $this->query($query);
  224.  
  225.         // Armazena uma cópia dos dados (não o resource, que é single-read)
  226.         $rows = [];
  227.         if ($this->query && is_object($this->query)) {
  228.             while ($row = $this->query->fetch_array(MYSQLI_ASSOC)) {
  229.                 $rows[] = $row;
  230.             }
  231.             $this->cache[$key] = ['result' => $rows, 'time' => $now];
  232.             // Cria um "resultado virtual" iterável via array
  233.             $this->query = $rows;
  234.         }
  235.  
  236.         return $this;
  237.     }
  238.  
  239.     /**
  240.      * Limpa todo o cache de queries ou uma entrada específica
  241.      *
  242.      * @param  string|null $query  SQL específico para limpar (null = limpa tudo)
  243.      */
  244.     public function clear_cache(?string $query = null): void {
  245.         if ($query === null) {
  246.             $this->cache = [];
  247.         } else {
  248.             unset($this->cache[md5($query)]);
  249.         }
  250.     }
  251.  
  252.     // -------------------------------------------------------------------------
  253.     // INSERT / UPDATE / DELETE
  254.     // -------------------------------------------------------------------------
  255.  
  256.     /**
  257.      * Insere um registro usando array associativo
  258.      * Usa escape em todos os valores
  259.      *
  260.      * @param  string $table  Nome da tabela
  261.      * @param  array  $array  Dados no formato ['coluna' => 'valor']
  262.      * @return $this
  263.      *
  264.      * @example
  265.      *   $db->insert_array('users', ['name' => 'Ana', 'email' => '[email protected]']);
  266.      */
  267.     public function insert_array(string $table, array $array): static {
  268.         foreach ($array as $key => $value) {
  269.             $columns[] = '`' . $key . '`';
  270.             $fields[]  = ($value !== '' && $value !== null)
  271.                 ? "'" . $this->escape((string) $value) . "'"
  272.                 : 'NULL';
  273.         }
  274.  
  275.         $columns = implode(', ', $columns);
  276.         $fields  = implode(', ', $fields);
  277.  
  278.         return $this->query("INSERT INTO `{$table}` ({$columns}) VALUES ({$fields});");
  279.     }
  280.  
  281.     /**
  282.      * Atualiza registros usando array associativo
  283.      * Usa escape em todos os valores
  284.      *
  285.      * @param  string $table  Nome da tabela
  286.      * @param  array  $array  Dados no formato ['coluna' => 'valor']
  287.      * @param  string $where  Condição WHERE (sem a palavra WHERE)
  288.      * @return $this
  289.      * @throws InvalidArgumentException
  290.      *
  291.      * @example
  292.      *   $db->update_array('users', ['name' => 'João'], "id = 5");
  293.      */
  294.     public function update_array(string $table, array $array, string $where): static {
  295.         if (empty($where)) {
  296.             throw new InvalidArgumentException(
  297.                 'update_array() requer uma condição WHERE para evitar atualização de todos os registros.'
  298.             );
  299.         }
  300.  
  301.         foreach ($array as $key => $value) {
  302.             $set[] = '`' . $key . '` = ' . (
  303.                 ($value !== '' && $value !== null)
  304.                     ? "'" . $this->escape((string) $value) . "'"
  305.                     : 'NULL'
  306.             );
  307.         }
  308.  
  309.         $set = implode(', ', $set);
  310.  
  311.         return $this->query("UPDATE `{$table}` SET {$set} WHERE {$where};");
  312.     }
  313.  
  314.     /**
  315.      * Exclui registros de uma tabela com condição obrigatória
  316.      *
  317.      * @param  string $table  Nome da tabela
  318.      * @param  string $where  Condição WHERE (sem a palavra WHERE)
  319.      * @return $this
  320.      * @throws InvalidArgumentException
  321.      *
  322.      * @example
  323.      *   $db->delete_where('users', "id = 10");
  324.      */
  325.     public function delete_where(string $table, string $where): static {
  326.         if (empty($where)) {
  327.             throw new InvalidArgumentException(
  328.                 'delete_where() requer uma condição WHERE para evitar exclusão de todos os registros.'
  329.             );
  330.         }
  331.  
  332.         return $this->query("DELETE FROM `{$table}` WHERE {$where};");
  333.     }
  334.  
  335.     // -------------------------------------------------------------------------
  336.     // FETCH
  337.     // -------------------------------------------------------------------------
  338.  
  339.     /**
  340.      * Retorna todos os resultados como array numérico e associativo
  341.      */
  342.     public function fetch_array(): array {
  343.         // Suporte a resultado em cache (array nativo)
  344.         if (is_array($this->query)) {
  345.             return $this->query;
  346.         }
  347.         while ($array[] = $this->query->fetch_array(MYSQLI_BOTH));
  348.         return array_filter($array);
  349.     }
  350.  
  351.     /**
  352.      * Retorna todos os resultados como array associativo
  353.      */
  354.     public function fetch_assoc(): array {
  355.         if (is_array($this->query)) {
  356.             return $this->query;
  357.         }
  358.         while ($array[] = $this->query->fetch_array(MYSQLI_ASSOC));
  359.         return array_filter($array);
  360.     }
  361.  
  362.     /**
  363.      * Retorna todos os resultados como array numérico
  364.      */
  365.     public function fetch_row(): array {
  366.         if (is_array($this->query)) {
  367.             return array_values($this->query);
  368.         }
  369.         while ($array[] = $this->query->fetch_array(MYSQLI_NUM));
  370.         return array_filter($array);
  371.     }
  372.  
  373.     /**
  374.      * Retorna apenas o primeiro resultado como array associativo (ou null)
  375.      *
  376.      * @example
  377.      *   $user = $db->query("SELECT * FROM users WHERE id = 1")->fetch_one();
  378.      */
  379.     public function fetch_one(): ?array {
  380.         if (is_array($this->query)) {
  381.             return $this->query[0] ?? null;
  382.         }
  383.         return $this->query->fetch_array(MYSQLI_ASSOC) ?: null;
  384.     }
  385.  
  386.     /**
  387.      * Retorna o valor de uma única coluna do primeiro resultado
  388.      *
  389.      * @param  string|int $column  Nome ou índice da coluna (padrão: primeira coluna)
  390.      * @return mixed
  391.      *
  392.      * @example
  393.      *   $total = $db->query("SELECT COUNT(*) FROM users")->fetch_value();
  394.      *   $email = $db->query("SELECT email FROM users WHERE id = 1")->fetch_value('email');
  395.      */
  396.     public function fetch_value(string|int $column = 0): mixed {
  397.         if (is_array($this->query)) {
  398.             $row = $this->query[0] ?? null;
  399.             return $row ? (is_int($column) ? array_values($row)[$column] : $row[$column]) : null;
  400.         }
  401.         $row = $this->query->fetch_array(MYSQLI_BOTH);
  402.         return $row[$column] ?? null;
  403.     }
  404.  
  405.     /**
  406.      * Retorna os nomes das colunas do resultado
  407.      */
  408.     public function fetch_field(): array {
  409.         while ($field = $this->query->fetch_field()) {
  410.             $array[] = $field->name;
  411.         }
  412.         return $array ?? [];
  413.     }
  414.  
  415.     // -------------------------------------------------------------------------
  416.     // PAGINAÇÃO
  417.     // -------------------------------------------------------------------------
  418.  
  419.     /**
  420.      * Executa uma query com paginação automática (LIMIT + OFFSET)
  421.      *
  422.      * @param  string $query     SQL sem LIMIT/OFFSET
  423.      * @param  int    $page      Número da página (começa em 1)
  424.      * @param  int    $per_page  Registros por página
  425.      * @return $this
  426.      *
  427.      * @example
  428.      *   $db->paginate("SELECT * FROM products ORDER BY name", 2, 10)->fetch_assoc();
  429.      */
  430.     public function paginate(string $query, int $page = 1, int $per_page = 10): static {
  431.         $page   = max(1, $page);
  432.         $offset = ($page - 1) * $per_page;
  433.  
  434.         return $this->query($query . " LIMIT {$per_page} OFFSET {$offset}");
  435.     }
  436.  
  437.     /**
  438.      * Conta o total de registros de uma tabela com condição opcional
  439.      *
  440.      * @param  string $table  Nome da tabela
  441.      * @param  string $where  Condição WHERE opcional (padrão: todos os registros)
  442.      * @return int
  443.      *
  444.      * @example
  445.      *   $total = $db->count('users');
  446.      *   $ativos = $db->count('users', "active = 1");
  447.      */
  448.     public function count(string $table, string $where = '1'): int {
  449.         return (int) $this->query(
  450.             "SELECT COUNT(*) FROM `{$table}` WHERE {$where}"
  451.         )->fetch_value();
  452.     }
  453.  
  454.     // -------------------------------------------------------------------------
  455.     // TRANSAÇÕES
  456.     // -------------------------------------------------------------------------
  457.  
  458.     /**
  459.      * Inicia uma transação
  460.      *
  461.      * @return $this
  462.      *
  463.      * @example
  464.      *   $db->begin_transaction();
  465.      *   try {
  466.      *       $db->insert_array('accounts', [...]);
  467.      *       $db->update_array('balances', [...], "id = 1");
  468.      *       $db->commit();
  469.      *   } catch (Exception $e) {
  470.      *       $db->rollback();
  471.      *   }
  472.      */
  473.     public function begin_transaction(): static {
  474.         mysqli_autocommit($this->connect, false);
  475.         mysqli_begin_transaction($this->connect);
  476.         $this->in_transaction = true;
  477.         return $this;
  478.     }
  479.  
  480.     /**
  481.      * Confirma a transação atual
  482.      *
  483.      * @return $this
  484.      * @throws RuntimeException
  485.      */
  486.     public function commit(): static {
  487.         if (!$this->in_transaction) {
  488.             throw new RuntimeException('Nenhuma transação ativa para confirmar.');
  489.         }
  490.         mysqli_commit($this->connect);
  491.         mysqli_autocommit($this->connect, true);
  492.         $this->in_transaction = false;
  493.         return $this;
  494.     }
  495.  
  496.     /**
  497.      * Desfaz a transação atual
  498.      *
  499.      * @return $this
  500.      */
  501.     public function rollback(): static {
  502.         if ($this->in_transaction) {
  503.             mysqli_rollback($this->connect);
  504.             mysqli_autocommit($this->connect, true);
  505.             $this->in_transaction = false;
  506.         }
  507.         return $this;
  508.     }
  509.  
  510.     /**
  511.      * Verifica se há uma transação ativa
  512.      */
  513.     public function is_in_transaction(): bool {
  514.         return $this->in_transaction;
  515.     }
  516.  
  517.     // -------------------------------------------------------------------------
  518.     // SEGURANÇA E UTILITÁRIOS
  519.     // -------------------------------------------------------------------------
  520.  
  521.     /**
  522.      * Escapa uma string para uso seguro em queries
  523.      *
  524.      * @param  string $value  Valor a escapar
  525.      * @return string
  526.      *
  527.      * @example
  528.      *   $nome = $db->escape($_POST['nome']);
  529.      *   $db->query("SELECT * FROM users WHERE name = '{$nome}'");
  530.      */
  531.     public function escape(string $value): string {
  532.         return mysqli_real_escape_string($this->connect, $value);
  533.     }
  534.  
  535.     /**
  536.      * Executa EXPLAIN na última query e retorna o plano de execução
  537.      * Útil para identificar queries lentas e falta de índices
  538.      *
  539.      * @return array
  540.      *
  541.      * @example
  542.      *   $db->query("SELECT * FROM orders WHERE user_id = 1");
  543.      *   $plan = $db->explain();
  544.      */
  545.     public function explain(): array {
  546.         if (empty($this->last)) {
  547.             return [];
  548.         }
  549.         return $this->query("EXPLAIN {$this->last}")->fetch_assoc();
  550.     }
  551.  
  552.     // -------------------------------------------------------------------------
  553.     // INFORMAÇÕES DO RESULTADO
  554.     // -------------------------------------------------------------------------
  555.  
  556.     /** Número de linhas retornadas */
  557.     public function num_rows(): int {
  558.         if (is_array($this->query)) {
  559.             return count($this->query);
  560.         }
  561.         return mysqli_num_rows($this->query);
  562.     }
  563.  
  564.     /** Número de colunas no resultado */
  565.     public function num_fields(): int {
  566.         return mysqli_field_count($this->connect);
  567.     }
  568.  
  569.     /** Número de linhas afetadas pela última operação */
  570.     public function affected_rows(): int {
  571.         return mysqli_affected_rows($this->connect);
  572.     }
  573.  
  574.     /** ID gerado pelo último INSERT */
  575.     public function insert_id(): int {
  576.         return mysqli_insert_id($this->connect);
  577.     }
  578.  
  579.     // -------------------------------------------------------------------------
  580.     // HISTÓRICO E PERFORMANCE
  581.     // -------------------------------------------------------------------------
  582.  
  583.     /**
  584.      * Retorna o histórico completo de queries executadas
  585.      */
  586.     public function get_history(): array {
  587.         return $this->history;
  588.     }
  589.  
  590.     /**
  591.      * Retorna métricas de performance das queries
  592.      * Inclui total de queries, tempo total e tempo médio
  593.      *
  594.      * @return array
  595.      *
  596.      * @example
  597.      *   $metrics = $db->get_profile();
  598.      *   echo "Queries: {$metrics['total']} | Tempo total: {$metrics['total_ms']}ms";
  599.      */
  600.     public function get_profile(): array {
  601.         $total = count($this->profile);
  602.         $sum   = array_sum($this->profile);
  603.  
  604.         return [
  605.             'total'    => $total,
  606.             'total_ms' => round($sum, 3),
  607.             'avg_ms'   => $total > 0 ? round($sum / $total, 3) : 0,
  608.             'slowest'  => $total > 0 ? max($this->profile) : 0,
  609.             'queries'  => $this->profile,
  610.         ];
  611.     }
  612.  
  613.     // -------------------------------------------------------------------------
  614.     // MEMÓRIA
  615.     // -------------------------------------------------------------------------
  616.  
  617.     /**
  618.      * Libera o resultado atual da memória
  619.      */
  620.     public function free_result(): bool {
  621.         if (is_object($this->query)) {
  622.             return mysqli_free_result($this->query);
  623.         }
  624.         return false;
  625.     }
  626.  
  627. }
Advertisement
Add Comment
Please, Sign In to add comment