Implementação técnica de fóruns de discussão estilo "construção de torres" em PHP

Objetivo e Funcionalidade

O objetivo é implementar uma funcionalidade semelhante ao sistema de rsepostas em árvore do Baidu Tieba, onde os tópicos são exibidos em estrutura hierárquica sem repetição de mensagens.

Exemplo de Estrutura

A visualização segue um padrão de indentação que representa o nível de profundidade:

* Resposta //Post 1
** Resposta //Resposta filha do Post 1
*** Resposta //Resposta neta do Post 1
**** Resposta //Resposta bisneta do Post 1
***** Resposta //...
****** Resposta
******* Resposta
******** Resposta
********* Resposta
********** Resposta
*********** Resposta
************ Resposta
* Resposta //Post 2
** Resposta //Filho do Post 2
* Resposta //Post 3
** Resposta //...

Estratégia de Implementação

Existem duas abordagens principais:

  1. Carregamento completo: Buscar todas as respostas em memória e organizar em estrutura de árvore
  2. Carregametno incremental: Obter apenas respostas de primeiro nível, carregar sub-respsotas via AJAX conforme necessário

Estrutura de Dados no Banco

Tabela MySQL com campos para manter relações hierárquicas:

Campo Descrição
id ID único da mensagem
root_id ID da mensagem raiz
parent_id ID da mensagem pai
next_sibling_id ID da próxima mensagem irmã
first_child_id ID da primeira resposta filha
last_child_id ID da última resposta filha
nivel Nível de profundidade
tempo_int Timestamp da postagem
tempo_str Data/hora em formato string
conteudo Conteúdo da mensagem

Operações de Inserção

Código para adicionar mensagens raiz:

public function criarMensagemRaiz($conteudo = 'Nova Mensagem') {
    $dados = [
        'root_id' => 0,
        'parent_id' => 0,
        'next_sibling_id' => 0,
        'first_child_id' => 0,
        'nivel' => 0,
        'conteudo' => $conteudo
    ];

    $timestamp = time();
    $data_formatada = date('YmdH', $timestamp);

    $dados['tempo_int'] = $timestamp;
    $dados['tempo_str'] = $data_formatada;

    return $this->conexaoBD->inserir('mensagens', $dados);
}

Código para adicionar respostas:

public function adicionarResposta($mensagem_pai_id, $conteudo = 'Resposta') {
    $consulta = "id={$mensagem_pai_id}";
    $resultado = $this->conexaoBD->buscarUm($consulta);
    
    $id_atual = $resultado['id'];
    $id_raiz = $resultado['root_id'];
    $primeiro_filho = $resultado['first_child_id'];
    $ultimo_filho = $resultado['last_child_id'];
    $nivel_atual = $resultado['nivel'];

    $dados_resposta = [
        'parent_id' => $mensagem_pai_id,
        'next_sibling_id' => 0,
        'first_child_id' => 0,
        'conteudo' => $conteudo
    ];

    $dados_resposta['root_id'] = $nivel_atual ? $id_raiz : $id_atual;
    
    $timestamp = time();
    $data_formatada = date('YmdH', $timestamp);
    
    $dados_resposta['nivel'] = ++$nivel_atual;
    $dados_resposta['tempo_int'] = $timestamp;
    $dados_resposta['tempo_str'] = $data_formatada;

    $novo_id = $this->conexaoBD->inserir('mensagens', $dados_resposta);

    if (!$primeiro_filho) {
        $condicao = "id = {$id_atual}";
        $atualizacao = ['first_child_id' => $novo_id];
        $this->conexaoBD->atualizar('mensagens', $atualizacao, $condicao);
    }

    if ($ultimo_filho) {
        $condicao = "id = {$ultimo_filho}";
        $atualizacao = ['next_sibling_id' => $novo_id];
        $this->conexaoBD->atualizar('mensagens', $atualizacao, $condicao);
    }

    $condicao = "id = {$id_atual}";
    $atualizacao = ['last_child_id' => $novo_id];
    $this->conexaoBD->atualizar('mensagens', $atualizacao, $condicao);
    
    return $novo_id;
}

Recuperação e Ordenação de Dados

Método Recursivo

public function obterDetalhesMensagem($id_raiz) {
    $this->id_raiz_atual = $id_raiz;
    
    $condicao = 'id = ' . $id_raiz;
    $raiz = $this->conexaoBD->buscarUm($condicao);
    $primeiro_filho = $raiz['first_child_id'];

    $condicao = 'root_id = ' . $id_raiz;
    $this->lista_mensagens = $this->conexaoBD->buscarTodos($condicao, '', '', '', 'id');

    $this->processarHierarquia($this->lista_mensagens[$primeiro_filho]);

    return [
        'mensagens' => $this->lista_mensagens,
        'ordem' => $this->ordem_processamento
    ];
}

private function processarHierarquia($no_atual) {
    $this->ordem_processamento[$no_atual['id']] = $no_atual['id'];
    
    if ($no_atual['first_child_id'] && empty($this->ordem_processamento[$no_atual['first_child_id']])) {
        $this->processarHierarquia($this->lista_mensagens[$no_atual['first_child_id']]);
    } elseif ($no_atual['next_sibling_id']) {
        $this->processarHierarquia($this->lista_mensagens[$no_atual['next_sibling_id']]);
    } elseif ($this->lista_mensagens[$no_atual['parent_id']]['next_sibling_id']) {
        $this->processarHierarquia($this->lista_mensagens[$this->lista_mensagens[$no_atual['parent_id']]['next_sibling_id']]);
    } elseif ($no_atual['parent_id'] != $this->id_raiz_atual) {
        $this->processarHierarquia($this->lista_mensagens[$no_atual['parent_id']]);
    }
}

Método Iterativo

public function obterComEstruturaIterativa($id_raiz) {
    $this->id_raiz_atual = $id_raiz;
    
    $condicao = 'id = ' . $id_raiz;
    $raiz = $this->conexaoBD->buscarUm($condicao);
    $primeiro_filho = $raiz['first_child_id'];

    $condicao = 'root_id = ' . $id_raiz;
    $ordem = 'id';
    $this->lista_mensagens = $this->conexaoBD->buscarTodos($condicao, '', $ordem, '', 'id');
    
    $this->processarIterativamente($raiz);
    $this->lista_mensagens[$id_raiz] = $raiz;
    unset($this->ordem_processamento[0]);

    return [
        'mensagens' => $this->lista_mensagens,
        'raiz' => $raiz,
        'ordem' => $this->ordem_processamento
    ];
}

private function processarIterativamente($mensagem_raiz) {
    $this->ordem_processamento[] = $mensagem_raiz['id'];
    $this->ordem_processamento[] = $mensagem_raiz['first_child_id'];
    $this->ordem_processamento[] = $mensagem_raiz['last_child_id'];

    foreach ($this->lista_mensagens as $id_atual => $valor) {
        $chave_atual = array_search($id_atual, $this->ordem_processamento);
        
        $primeiro_filho = $valor['first_child_id'];
        $ultimo_filho = $valor['last_child_id'];
        $irmao_seguinte = $valor['next_sibling_id'];

        if ($primeiro_filho && ($primeiro_filho != $this->ordem_processamento[$chave_atual + 1])) {
            array_splice($this->ordem_processamento, $chave_atual + 1, 0, $primeiro_filho);
            if ($ultimo_filho && ($ultimo_filho != $primeiro_filho)) {
                array_splice($this->ordem_processamento, $chave_atual + 2, 0, $ultimo_filho);
            }
        }

        if ($irmao_seguinte) {
            $chave_irmao = array_search($irmao_seguinte, $this->ordem_processamento);
            if (!$chave_irmao) {
                if ($ultimo_filho) {
                    $chave_ultimo = array_search($ultimo_filho, $this->ordem_processamento);
                    array_splice($this->ordem_processamento, $chave_ultimo + 1, 0, $irmao_seguinte);
                } elseif ($primeiro_filho) {
                    array_splice($this->ordem_processamento, $chave_atual + 2, 0, $irmao_seguinte);
                } else {
                    array_splice($this->ordem_processamento, $chave_atual + 1, 0, $irmao_seguinte);
                }
            }
        }
    }
}

Renderização HTML

<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <?php
        echo $raiz['conteudo'], ' >>> Autor '.$raiz['id'].' Data: ', $raiz['tempo_str'], '<hr>';
        foreach ($ordem as $valor) {
            for($contador = 0; $contador < $mensagens[$valor]['nivel']; ++$contador) {
                echo '|-';
            }
            $id_atual = $mensagens[$valor]['id'];
            $id_raiz_atual = $mensagens[$valor]['root_id'];
            echo $id_atual.'diz: @'. $mensagens[$mensagens[$valor]['parent_id']]['id']. ' ' .$mensagens[$valor]['conteudo'].' ['.$mensagens[$valor]['tempo_str']."] <a href='{$url_controle}/responder?id={$id_atual}&rootid={$id_raiz_atual}'><Responder></a><br>";
        } 
    ?>
</body>
</html>

Abordagem Prática

Na maioria dos casos, as respostas têm apenas um nível de profundidade. Uma abordagem mais eficiente seria:

  1. Carregar apenas as respostas de primeiro nível no servidor
  2. Usar AJAX para buscar respostas aninhadas dinamicamente

Método para obter respostas de primeiro nível:

public function buscarRespostasNivel1($id_raiz) {
    $condicao = "root_id = {$id_raiz} AND nivel = 1";
    return $this->conexaoBD->buscarTodos($condicao);
}

Essa abordagem oferece melhor desempenho para casos reais, evita sobrecarga de dados desnecessários e melhora a experiência do usuário com carregamento progressivo.

Tags: PHP MySQL forum hierarchical-comments tree-structure

Publicado em 9-8 20:43