[MU] Advanced Database Cleaner

Aula: Limpeza de Banco de Dados com MU-Plugin – Substituindo o Advanced Database Cleaner

Objetivo: Substituir um plugin robusto como o Advanced Database Cleaner por um MU-Plugin enxuto, que mantém o banco de dados limpo removendo revisões antigas, transients expirados, rascunhos automáticos, lixeira, comentários spam e outros resíduos deixados por temas e plugins desinstalados.

Contexto: O Advanced Database Cleaner é um plugin popular com interface gráfica e opções avançadas, mas muitas de suas funções podem ser reproduzidas com código eficiente e sem sobrecarga. Vamos criar um MU-Plugin que realiza a limpeza automática via WP-Cron, sem necessidade de interface administrativa, tornando o processo transparente e de fácil manutenção.

Funcionalidades implementadas no nosso MU-Plugin

  • Remoção de revisões antigas de posts/páginas (padrão: 30 dias)
  • Remoção de rascunhos automáticos (auto-drafts)
  • Remoção de posts na lixeira (trash)
  • Remoção de comentários spam, pendentes e na lixeira
  • Remoção de pingbacks e trackbacks
  • Remoção de transients expirados (lixo temporário do WordPress)
  • Remoção de meta dados órfãos (postmeta, commentmeta, usermeta, termmeta) – dados deixados por plugins desinstalados
  • Remoção de relacionamentos órfãos (term_relationships)
  • Remoção de opções órfãs na tabela wp_options (resíduos de plugins removidos)
  • Remoção de cache oEmbed
  • Execução automática via WP-Cron (diária)
  • Log das operações para depuração (opcional, ativado via constante)

Código do MU-Plugin

Crie a pasta wp-content/mu-plugins/neodoor-db-cleaner/ e dentro dela o arquivo neodoor-db-cleaner.php com o conteúdo abaixo:

<?php
/**
 * Plugin Name:  Neodoor Database Cleaner
 * Description:  Limpeza automática e segura do banco de dados: revisões antigas, transients expirados, lixeira, spam, metadados órfãos e resíduos de plugins.
 * Version:      1.0
 * Author:       Reinaldo (Neodoor.neo)
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Neodoor_DB_Cleaner {

    private static $instance = null;

    // Configurações (podem ser alteradas aqui ou via constantes)
    private $keep_revisions_days = 30;
    private $log_enabled = false;

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        // Carrega configurações de constantes, se definidas
        if ( defined( 'NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS' ) ) {
            $this->keep_revisions_days = intval( NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS );
        }
        if ( defined( 'NEODOOR_DB_CLEANER_LOG_ENABLED' ) ) {
            $this->log_enabled = (bool) NEODOOR_DB_CLEANER_LOG_ENABLED;
        }

        // Agenda a limpeza diária (se não estiver agendada)
        add_action( 'wp', [ $this, 'schedule_cleanup' ] );
        add_action( 'neodoor_db_daily_cleanup', [ $this, 'run_full_cleanup' ] );
    }

    /**
     * Agenda o evento de limpeza diária (hook: neodoor_db_daily_cleanup)
     */
    public function schedule_cleanup() {
        if ( ! wp_next_scheduled( 'neodoor_db_daily_cleanup' ) ) {
            wp_schedule_event( time(), 'daily', 'neodoor_db_daily_cleanup' );
            $this->log( 'Evento de limpeza diária agendado.' );
        }
    }

    /**
     * Executa todas as rotinas de limpeza
     */
    public function run_full_cleanup() {
        global $wpdb;
        
        $this->log( 'Iniciando limpeza completa do banco de dados.' );
        
        // 1. Remove revisões antigas
        $this->delete_old_revisions();
        
        // 2. Remove rascunhos automáticos
        $this->delete_auto_drafts();
        
        // 3. Remove posts na lixeira
        $this->delete_trashed_posts();
        
        // 4. Remove comentários spam, pendentes e na lixeira
        $this->delete_spam_comments();
        $this->delete_pending_comments();
        $this->delete_trashed_comments();
        
        // 5. Remove pingbacks e trackbacks
        $this->delete_pingbacks_trackbacks();
        
        // 6. Remove transients expirados
        $this->delete_expired_transients();
        
        // 7. Remove metadados órfãos
        $this->delete_orphaned_post_meta();
        $this->delete_orphaned_comment_meta();
        $this->delete_orphaned_user_meta();
        $this->delete_orphaned_term_meta();
        
        // 8. Remove relacionamentos órfãos
        $this->delete_orphaned_term_relationships();
        
        // 9. Remove opções órfãs (resíduos de plugins desinstalados)
        $this->delete_orphaned_options();
        
        // 10. Remove cache oEmbed
        $this->delete_oembed_cache();
        
        // 11. Otimiza as tabelas (opcional)
        $this->optimize_tables();
        
        $this->log( 'Limpeza completa finalizada.' );
    }
    
    /**
     * Remove revisões de posts/páginas com mais de X dias
     */
    private function delete_old_revisions() {
        global $wpdb;
        $sql = $wpdb->prepare(
            "DELETE FROM {$wpdb->posts} 
             WHERE post_type = 'revision' 
             AND post_date < NOW() - INTERVAL %d DAY", $this->keep_revisions_days
        );
        $deleted = $wpdb->query( $sql );
        $this->log( "Revisões antigas removidas: {$deleted}" );
    }
    
    /**
     * Remove rascunhos automáticos
     */
    private function delete_auto_drafts() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->posts} 
             WHERE post_status = 'auto-draft'"
        );
        $this->log( "Rascunhos automáticos removidos: {$deleted}" );
    }
    
    /**
     * Remove posts na lixeira
     */
    private function delete_trashed_posts() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->posts} 
             WHERE post_status = 'trash'"
        );
        $this->log( "Posts na lixeira removidos: {$deleted}" );
    }
    
    /**
     * Remove comentários com status 'spam'
     */
    private function delete_spam_comments() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->comments} 
             WHERE comment_approved = 'spam'"
        );
        $this->log( "Comentários spam removidos: {$deleted}" );
    }
    
    /**
     * Remove comentários pendentes (moderação)
     */
    private function delete_pending_comments() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->comments} 
             WHERE comment_approved = '0'"
        );
        $this->log( "Comentários pendentes removidos: {$deleted}" );
    }
    
    /**
     * Remove comentários na lixeira
     */
    private function delete_trashed_comments() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->comments} 
             WHERE comment_approved = 'trash'"
        );
        $this->log( "Comentários na lixeira removidos: {$deleted}" );
    }
    
    /**
     * Remove pingbacks e trackbacks
     */
    private function delete_pingbacks_trackbacks() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->comments} 
             WHERE comment_type IN ('pingback', 'trackback')"
        );
        $this->log( "Pingbacks/trackbacks removidos: {$deleted}" );
    }
    
    /**
     * Remove transients expirados (dados temporários)
     */
    private function delete_expired_transients() {
        global $wpdb;
        $timestamp = time();
        // Remove transients expirados em lote para evitar timeout
        $batch = 50;
        $total_deleted = 0;
        
        while ( true ) {
            // Busca chaves de timeout expiradas
            $timeout_keys = $wpdb->get_col( $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} 
                 WHERE option_name LIKE '_transient_timeout_%' 
                 AND CAST(option_value AS UNSIGNED) < %d LIMIT %d", $timestamp, $batch ) ); if ( empty( $timeout_keys ) ) { break; } // Prepara as chaves dos transients correspondentes $transient_keys = []; foreach ( $timeout_keys as $key ) { $transient_keys[] = '_transient_' . substr( $key, 19 ); } // Junta todas as chaves para remoção $all_keys = array_merge( $timeout_keys, $transient_keys ); $placeholders = implode( ', ', array_fill( 0, count( $all_keys ), '%s' ) ); $deleted = $wpdb->query( $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name IN ({$placeholders})",
                $all_keys
            ) );
            
            $total_deleted += $deleted;
            
            if ( count( $timeout_keys ) < $batch ) { break; } } $this->log( "Transients expirados removidos: {$total_deleted}" );
    }
    
    /**
     * Remove postmeta órfão (sem post correspondente)
     */
    private function delete_orphaned_post_meta() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE pm FROM {$wpdb->postmeta} pm
             LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
             WHERE p.ID IS NULL"
        );
        $this->log( "Postmeta órfão removido: {$deleted}" );
    }
    
    /**
     * Remove commentmeta órfão (sem comment correspondente)
     */
    private function delete_orphaned_comment_meta() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE cm FROM {$wpdb->commentmeta} cm
             LEFT JOIN {$wpdb->comments} c ON c.comment_ID = cm.comment_id
             WHERE c.comment_ID IS NULL"
        );
        $this->log( "Commentmeta órfão removido: {$deleted}" );
    }
    
    /**
     * Remove usermeta órfão (sem user correspondente)
     */
    private function delete_orphaned_user_meta() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE um FROM {$wpdb->usermeta} um
             LEFT JOIN {$wpdb->users} u ON u.ID = um.user_id
             WHERE u.ID IS NULL"
        );
        $this->log( "Usermeta órfão removido: {$deleted}" );
    }
    
    /**
     * Remove termmeta órfão (sem term correspondente)
     */
    private function delete_orphaned_term_meta() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE tm FROM {$wpdb->termmeta} tm
             LEFT JOIN {$wpdb->terms} t ON t.term_id = tm.term_id
             WHERE t.term_id IS NULL"
        );
        $this->log( "Termmeta órfão removido: {$deleted}" );
    }
    
    /**
     * Remove relacionamentos term_relationships órfãos (sem post ou term correspondente)
     */
    private function delete_orphaned_term_relationships() {
        global $wpdb;
        // Remove relacionamentos sem post
        $deleted1 = $wpdb->query(
            "DELETE tr FROM {$wpdb->term_relationships} tr
             LEFT JOIN {$wpdb->posts} p ON p.ID = tr.object_id
             WHERE p.ID IS NULL"
        );
        // Remove relacionamentos sem term_taxonomy
        $deleted2 = $wpdb->query(
            "DELETE tr FROM {$wpdb->term_relationships} tr
             LEFT JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
             WHERE tt.term_taxonomy_id IS NULL"
        );
        $total = $deleted1 + $deleted2;
        $this->log( "Relacionamentos term_relationships órfãos removidos: {$total}" );
    }
    
    /**
     * Remove opções órfãs (resíduos de plugins desinstalados)
     * Lista negra básica de prefixos comuns de plugins.
     */
    private function delete_orphaned_options() {
        global $wpdb;
        
        // Lista de prefixos de opções que geralmente são resíduos
        $prefixes = [
            'widget_', 'cron', 'recently_activated', 'wpseo_', '_transient_',
            '_site_transient_', 'rewrite_rules', 'auto_update_', 'wp_user_roles'
        ];
        
        // Constrói a cláusula NOT LIKE para cada prefixo (mantém os essenciais)
        $not_like = "";
        foreach ( $prefixes as $prefix ) {
            $not_like .= $wpdb->prepare( " AND option_name NOT LIKE %s", $prefix . '%' );
        }
        
        // Busca opções que não correspondem a plugins ativos ou temas (simplificado)
        $sql = "SELECT option_name FROM {$wpdb->options} 
                WHERE (option_name LIKE 'wp_%' OR option_name LIKE 'woocommerce_%' 
                       OR option_name LIKE 'elementor_%' OR option_name LIKE 'jetpack_%')
                AND autoload = 'no' {$not_like}
                LIMIT 200";
        
        $options_to_delete = $wpdb->get_col( $sql );
        
        if ( ! empty( $options_to_delete ) ) {
            $placeholders = implode( ', ', array_fill( 0, count( $options_to_delete ), '%s' ) );
            $deleted = $wpdb->query( $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name IN ({$placeholders})",
                $options_to_delete
            ) );
            $this->log( "Opções órfãs removidas: {$deleted}" );
        } else {
            $this->log( "Nenhuma opção órfã encontrada para remoção." );
        }
    }
    
    /**
     * Remove cache oEmbed (transients específicos)
     */
    private function delete_oembed_cache() {
        global $wpdb;
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->postmeta} 
             WHERE meta_key LIKE '_oembed_%'"
        );
        $this->log( "Cache oEmbed removido: {$deleted} registros" );
    }
    
    /**
     * Otimiza todas as tabelas do banco de dados (opcional)
     * Pode ser pesado em sites grandes; desabilite se necessário.
     */
    private function optimize_tables() {
        global $wpdb;
        $tables = $wpdb->get_col( "SHOW TABLES" );
        $optimized = 0;
        foreach ( $tables as $table ) {
            if ( $wpdb->query( "OPTIMIZE TABLE {$table}" ) ) {
                $optimized++;
            }
        }
        $this->log( "Tabelas otimizadas: {$optimized} de " . count( $tables ) );
    }
    
    /**
     * Função de log simples (escreve em arquivo se habilitado)
     */
    private function log( $message ) {
        if ( ! $this->log_enabled ) {
            return;
        }
        $log_file = WP_CONTENT_DIR . '/db-cleaner-log.txt';
        $timestamp = current_time( 'mysql' );
        $entry = "[{$timestamp}] {$message}" . PHP_EOL;
        error_log( $entry, 3, $log_file );
    }
}

Neodoor_DB_Cleaner::get_instance();

Instruções de instalação

  1. Certifique-se de que o autoloader loader.php esteja presente em /mu-plugins/ (conforme aula anterior).
  2. Crie a pasta neodoor-db-cleaner dentro de /mu-plugins/.
  3. Dentro dela, crie o arquivo neodoor-db-cleaner.php e cole o código acima.
  4. Desative e remova o plugin advanced-database-cleaner da lista de plugins.
  5. O MU-Plugin começará a funcionar imediatamente e agendará a limpeza diária automaticamente.

Configuração avançada (via constantes no wp-config.php)

Você pode ajustar o comportamento do plugin definindo as seguintes constantes no arquivo wp-config.php:

// Mantém revisões dos últimos 60 dias (padrão: 30)
define( 'NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS', 60 );

// Habilita logs de depuração (arquivo /wp-content/db-cleaner-log.txt)
define( 'NEODOOR_DB_CLEANER_LOG_ENABLED', true );

Teste manual da limpeza

Para executar a limpeza imediatamente (sem aguardar o cron diário), adicione o seguinte trecho temporariamente no tema ou em um plugin de teste:

do_action( 'neodoor_db_daily_cleanup' );

Após executar, remova o trecho para não rodar duas vezes.

Verificação e monitoramento

  • phpMyAdmin: Acesse seu banco de dados e verifique a redução no tamanho das tabelas wp_posts, wp_postmeta e wp_options.
  • Logs: Se habilitado, consulte o arquivo /wp-content/db-cleaner-log.txt para ver o que foi removido.
  • Performance: Monitore o tempo de carregamento do site antes e depois da limpeza.

Limitações e cuidados

  • Sempre faça backup do banco de dados antes de executar limpezas agressivas. Em ambiente local isso é menos crítico, mas é uma boa prática.
  • A remoção de opções órfãs é conservadora (mantém opções com prefixos conhecidos como widget_, cron, etc.) para não quebrar funcionalidades. Se precisar de limpeza mais profunda, revise a lista de exclusão.
  • Em sites muito grandes (milhares de posts), a limpeza de transients e metadados pode levar alguns segundos. O código usa batch para evitar timeout.
  • Se você usa cache externo (Redis, Memcached), os transients podem não estar na base de dados – nesse caso, a remoção via SQL não terá efeito.
  • Para uma limpeza ainda mais completa (tabelas órfãs, detecção de plugins inativos), recomenda-se executar manualmente scripts SQL específicos ou usar o WP-CLI.

Comparação com o plugin original

Nosso MU-Plugin substitui as principais funções do Advanced Database Cleaner, com as seguintes diferenças:

  • Sem interface administrativa: Tudo ocorre em segundo plano, sem botões ou telas para configurar.
  • Mais leve: Carrega apenas o necessário, sem CSS/JS desnecessários.
  • Transparente: O código está visível e pode ser ajustado conforme sua necessidade.
  • Agendamento fixo: Executa diariamente. Para outras frequências, altere a constante em wp_schedule_event.

Registro no caderno

Data:

Funcionalidade: Limpeza automática do banco de dados (revisões, transients, metadados órfãos, etc.)

Plugins substituídos: advanced-database-cleaner

Tempo de implementação: ~30 minutos (configuração e testes)

Observação: Em produção, execute a limpeza manualmente antes de ativar o cron para evitar picos de carga.

 


Explicação técnica sobre o ‘prefixo’

O prefixo NEODOOR (ou Neodoor_) é apenas um identificador único que usamos para:

  • Nomear classes (ex: Neodoor_Disable_RightClick) – para evitar conflito com outras classes que por acaso tenham o mesmo nome.

  • Nomear constantes (ex: NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS) – para evitar conflitos globais.

  • Nomear hooks/custom actions (ex: neodoor_db_daily_cleanup) – para garantir que nenhum outro plugin use o mesmo nome de hook.

O WordPress não exige nenhum prefixo específico. A única regra é: use um prefixo único e consistente para tudo que você criar (classes, funções, constantes, hooks). Isso é uma boa prática de programação para evitar colisões com temas/plugins de terceiros.

O que acontece se você trocar NEODOOR por PORTANOVA?

  • O plugin continua funcionando normalmente – pois todas as referências internas estarão consistentes.

  • Nada quebra – desde que você substitua todas as ocorrências do antigo prefixo pelo novo no código.

Cuidados ao trocar

  1. Use “pesquisar e substituir” no seu editor de código (VS Code, Notepad++, etc.) para garantir que nenhuma ocorrência seja esquecida.

  2. Atenção a letras maiúsculas/minúsculas – Neodoor é diferente de NEODOOR. No código temos:

    • Neodoor_ (classe)

    • neodoor_ (hooks, funções)

    • NEODOOR_ (constantes)

    • Ao trocar, mantenha a mesma capitalização: Portanova_portanova_PORTANOVA_.

  3. Recomendo manter um padrão – por exemplo, sempre usar o nome do seu projeto (ex: portanova) em minúsculas para hooks e funções, e maiúsculas para constantes.

Vantagem de usar um prefixo personalizado

  • Identifica facilmente que aquele código foi criado por você.

  • Evita conflitos caso instale um plugin que use o mesmo nome de classe (ex: outra pessoa também criou uma classe chamada Disable_RightClick).

  • Mais fácil de dar manutenção em projetos grandes.

✅ Voto computado! Obrigado.

Média: 3.2 (27 votos)