Você provavelmente tem scripts em PHP que executam diversas tarefas, como limpar metadados órfãos de artigos ou excluir transients expirados. Com o tempo, essa coleção cresce e passa a viver em um arquivo de tema, em uma pasta de plugin ou em algum diretório pouco acessível. O Acorn ajuda a organizar essa desordem ao trazer o Artisan Console do Laravel para o WordPress.

Isso significa que você pode criar comandos WP-CLI personalizados com arquivos de classe estruturados que centralizam sua lógica de manutenção. Esses comandos são executados de forma consistente em desenvolvimento, teste e produção, usando indicadores de progresso, saída em tabela formatada, tratamento adequado de erros e muito mais. Em seguida, você pode acioná-los via SSH, agendá-los com cron jobs ou executá-los durante implantações.

Como instalar o Acorn e executar comandos

O primeiro passo é instalar as dependências necessárias. O Acorn precisa do PHP 8.2 ou superior, do Composer para gerenciar dependências e do WP-CLI em execução no seu servidor. A Kinsta inclui o WP-CLI em todos os planos de hospedagem, para que você possa começar a criar comandos imediatamente.

Você instala o Acorn via Composer usando composer require roots/acorn na raiz do projeto. Em seguida, adicione o código de inicialização ao arquivo functions.php do seu tema ou ao arquivo principal do seu plugin:

<?php
use Roots\Acorn\Application;
if (! class_exists(\Roots\Acorn\Application::class)) {
    wp_die(
        __('You need to install Acorn to use this site.', 'domain'),
        '',
        [
            'link_url' => 'https://roots.io/acorn/docs/installation/',
            'link_text' => __('Acorn Docs: Installation', 'domain'),
        ]
    );
}

add_action('after_setup_theme', function () {
    Application::configure()
        ->withProviders([
            App\Providers\ThemeServiceProvider::class,
        ])
        ->boot();
}, 0);

A configuração sem necessidade de ajustes armazena o cache do aplicativo e os registros no diretório de cache do WordPress em [wp-content]/cache/acorn/, com seus comandos no diretório app/ do tema.

A estrutura tradicional segue as convenções do Laravel, como diretórios dedicados para app/, config/, storage/ e resources/ na raiz do seu projeto. Você configura isso com uma linha:

wp acorn acorn:init storage && wp acorn vendor:publish --tag=acorn

Se você executar wp acorn list, isso verificará sua instalação, exibindo todos os comandos Acorn disponíveis. A partir desse ponto, todos os comandos personalizados que você criar serão armazenados no diretório app/Console/Commands/. O Acorn descobre automaticamente todas as classes de comando nesse local e as registra no WP-CLI.

Criando seu primeiro comando Artisan

O comando wp acorn make:command CleanupCommand Artisan gera seu arquivo com a estrutura que você precisa. Ele contém três elementos-chave que todo comando Artisan precisa:

  • Uma propriedade $signature que define o nome do comando e suas opções.
  • A propriedade $description para o texto de ajuda.
  • Um método handle() onde a lógica do seu comando reside.

Nesse caso, ele cria app/Console/Commands/CleanupCommand.php com uma estrutura básica de comando:

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CleanupCommand extends Command
{
    protected $signature = 'app:cleanup';
    protected $description = 'Command description';
    public function handle()
    {
        //
    }
}

A propriedade $signature usa uma sintaxe específica:

  • Um comando básico precisa apenas de um nome, como cleanup:run.
  • Você adiciona argumentos obrigatórios envolvendo-os em chaves, por exemplo cleanup:run {days}.
  • Argumentos opcionais recebem um ponto de interrogação: cleanup:run {days?}.
  • Opções usam dois hífens: cleanup:run {--force} {--limit=100}.

Em seguida, inclua uma descrição do comando e sua lógica básica:

protected $signature = 'cleanup:test {--dry-run : Preview changes without executing}';
protected $description = 'Test cleanup command';
public function handle()
{
    $dryRun = $this->option('dry-run');

    if ($dryRun) {
        $this->components->info('Running in dry-run mode');
    }

    $this->components->info('Cleanup command executed');

    return 0;
}

Você pode testar isso usando wp acorn cleanup:test --dry-run. O comando exibe mensagens formatadas usando o sistema de componentes do Artisan. O método $this->components->info() mostra mensagens de sucesso em verde. Você também pode usar $this->components->error() para erros, $this->components->warn() para avisos e $this->line() para saída de texto simples.

Como criar 3 comandos práticos de manutenção

A seguir estão alguns exemplos que ajudam você a lidar com tarefas de manutenção de banco de dados que surgem em muitos sites WordPress.

Embora cada comando inclua tratamento de erros e, em grande parte, siga os padrões de codificação do WordPress para manter seus dados seguros, você ainda deve usá-los como base para seus próprios projetos, em vez de simplesmente copiá-los e colá-los.

1. Limpando metadados órfãos de artigos

Os metadados de artigos permanecem no banco de dados após a exclusão dos artigos. Isso acontece quando plugins ignoram os hooks de exclusão do WordPress e deixam entradas de metadados apontando para artigos que já não existem mais. Com o tempo, esse inchaço desacelera suas consultas ao banco de dados.

Depois de criar o comando com wp acorn make:command CleanupOrphanedMeta, você pode começar com a estrutura da classe do comando e sua assinatura:

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CleanupOrphanedMeta extends Command
{
    protected $signature = 'maintenance:cleanup-orphaned-meta 
                            {--dry-run : Preview orphans without deleting}';
    
    protected $description = 'Remove orphaned post metadata from wp_postmeta';
    public function handle()
    {
        global $wpdb;

        $dryRun = $this->option('dry-run');

        $this->components->info('Scanning for orphaned post metadata...');

O comando usa uma consulta com LEFT JOIN para encontrar esses registros órfãos. O padrão verifica valores NULL na tabela posts. Se o join retornar NULL, os metadados pertencem a um artigo excluído:

        // Find orphaned metadata
        $orphans = $wpdb->get_results("
            SELECT pm.meta_id, pm.post_id, pm.meta_key 
            FROM {$wpdb->postmeta} pm
            LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
            WHERE p.ID IS NULL
            LIMIT 1000
        ");

        if (empty($orphans)) {
            $this->components->info('No orphaned metadata found');
            return 0;
        }

Quando o comando encontra órfãos, ele mostra uma contagem e exibe alguns registros de exemplo se você estiver no modo dry-run. Você precisa verificar o que está sendo excluído antes de confirmar:

        $count = count($orphans);
        $this->components->warn("Found {$count} orphaned metadata records");
        if ($dryRun) {
            $this->newLine();
            $this->line('Sample orphaned records:');

            foreach (array_slice($orphans, 0, 5) as $orphan) {
                $this->line("  → Post ID {$orphan->post_id}: {$orphan->meta_key}");
            }

            return 0;
        }

A exclusão real usa $wpdb->prepare() para evitar ataques de injeção de SQL. O comando processa 1.000 registros por vez, o que evita problemas de memória em sites com milhões de entradas de metadados:

        // Delete orphaned records
        $metaIds = array_map(function($orphan) {
            return $orphan->meta_id;
        }, $orphans);

        $placeholders = implode(',', array_fill(0, count($metaIds), '%d'));

        $deleted = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN ({$placeholders})",
                ...$metaIds
            )
        );

        if ($deleted === false) {
            $this->components->error('Failed to delete orphaned metadata');
            return 1;
        }

        $this->components->info("Deleted {$deleted} orphaned metadata records");
        return 0;
    }
}

Para executar o comando, use wp acorn maintenance:cleanup-orphaned-meta --dry-run.

2. Excluindo transients expirados

O WordPress armazena transients em wp_options com datas de expiração. Embora um cron job diário limpe esses dados, às vezes é necessário executar uma limpeza manual durante janelas de manutenção ou quando o inchaço de transients se torna um problema.

Depois de gerar o comando com wp acorn make:command CleanupTransients, você pode configurar a estrutura do comando:

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CleanupTransients extends Command
{
    protected $signature = 'maintenance:cleanup-transients';
    protected $description = 'Delete expired transients from wp_options';
    public function handle()
    {
        global $wpdb;

        $this->components->info('Deleting expired transients...');

Essa consulta de exclusão usa a sintaxe DELETE de várias tabelas para remover o transiente e sua opção de tempo limite de uma só vez. A consulta encontra registros de tempo limite em que o carimbo de data/hora de expiração já passou:

        // Delete expired regular transients
        $deleted = $wpdb->query(
            $wpdb->prepare(
                "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
                WHERE a.option_name LIKE %s
                AND a.option_name NOT LIKE %s
                AND b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12))
                AND b.option_value < %d",
                $wpdb->esc_like('_transient_') . '%',
                $wpdb->esc_like('_transient_timeout_') . '%',
                time()
            )
        );

Verifique também se há erros e acompanhe a contagem de exclusões:

        if ($deleted === false) {
            $this->components->error('Failed to delete transients');
            return 1;
        }

        $transientCount = $deleted;

Em instalações Multisite, o comando executa uma segunda consulta para transients do site. Eles usam prefixos de tabela diferentes, mas seguem o mesmo padrão de exclusão:

        // Delete expired site transients (multisite)
        if (is_multisite()) {
            $siteDeleted = $wpdb->query(
                $wpdb->prepare(
                    "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
                    WHERE a.option_name LIKE %s
                    AND a.option_name NOT LIKE %s
                    AND b.option_name = CONCAT('_site_transient_timeout_', SUBSTRING(a.option_name, 17))
                    AND b.option_value < %d",
                    $wpdb->esc_like('_site_transient_') . '%',
                    $wpdb->esc_like('_site_transient_timeout_') . '%',
                    time()
                )
            );

            if ($siteDeleted !== false) {
                $transientCount += $siteDeleted;
            }
        }

        $this->components->info("Deleted {$transientCount} expired transients");

        return 0;
    }
}

Para executar o comando, execute wp acorn maintenance:cleanup-transients.

3. Auditando opções com autoload

Opções com autoload são carregadas em todas as requisições que o seu site WordPress processa. Você pode começar a perceber lentidão e picos de consumo de memória quando esses dados ultrapassam 1MB. Este comando encontra suas maiores opções com autoload para que você possa identificar quais plugins estão causando o inchaço.

Primeiro, crie o comando de auditoria com wp acorn make:command AuditAutoload. Em seguida, defina a assinatura do comando com um limite configurável:

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class AuditAutoload extends Command
{
    protected $signature = 'maintenance:audit-autoload 
                            {--threshold=1000000 : Size threshold in bytes}';

    protected $description = 'Audit autoloaded options size';
    public function handle()
    {
        global $wpdb;

        $threshold = (int) $this->option('threshold');

        $this->components->info('Calculating autoloaded options size...');

A partir daí, calcule o tamanho total de todas as opções com autoload:

        // Get total autoload size
        $result = $wpdb->get_row(
            "SELECT 
                SUM(LENGTH(option_value)) as total_bytes,
                COUNT(*) as total_count
            FROM {$wpdb->options}
            WHERE autoload = 'yes'"
        );

        $totalBytes = (int) $result->total_bytes;
        $totalCount = (int) $result->total_count;
        $totalMb = round($totalBytes / 1024 / 1024, 2);

        $this->newLine();
        $this->line("Total autoloaded: {$totalMb} MB ({$totalCount} options)");

O comando executa uma consulta para opções acima do seu limite, ordena por tamanho e limita os resultados às 20 maiores:

        // Get largest autoloaded options
        $largeOptions = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT option_name, LENGTH(option_value) as size_bytes
                FROM {$wpdb->options}
                WHERE autoload = 'yes'
                AND LENGTH(option_value) > %d
                ORDER BY size_bytes DESC
                LIMIT 20",
                $threshold
            )
        );

        if (empty($largeOptions)) {
            $this->components->info('No options exceed the threshold');
            return 0;
        }

O método $this->table() do Artisan formata esses resultados como uma tabela ASCII: ler dados tabulares no seu terminal é melhor do que analisar a saída bruta da consulta:

        $this->newLine();
        $this->components->warn('Large autoloaded options:');
        $this->newLine();

        $tableData = [];

        foreach ($largeOptions as $option) {
            $sizeKb = round($option->size_bytes / 1024, 2);
            $tableData[] = [
                $option->option_name,
                $sizeKb . ' KB'
            ];
        }

        $this->table(
            ['Option Name', 'Size'],
            $tableData
        );

O comando gera um aviso quando o total de autoload ultrapassa 3MB, o que indica um problema de desempenho que você precisa resolver:

        if ($totalBytes > 3000000) {
            $this->newLine();
            $this->components->error('Warning: Total autoload exceeds 3MB');
        }

        return 0;
    }
}

Para executar a auditoria, use wp acorn maintenance:audit-autoload --threshold=500000.

Como acessar dados do WordPress dentro dos comandos

As funções do WordPress funcionam dentro dos métodos do seu comando porque o Acorn é inicializado dentro do ciclo de vida do WordPress. Isso significa que você pode chamar funções como get_posts() ou get_option() sem nenhuma configuração especial:

public function handle()
{
    $posts = get_posts([
        'post_type' => 'post',
        'post_status' => 'publish',
        'numberposts' => 10,
    ]);

    foreach ($posts as $post) {
        $this->line($post->post_title);
    }

    return 0;
}

Para consultas diretas ao banco de dados, declare o global $wpdb no início do seu método:

public function handle()
{
    global $wpdb;   

    $count = $wpdb->get_var(
        "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish'"
    );

    $this->line("Published posts: {$count}");

    return 0;
}

$wpdb->prepare() é ideal sempre que suas consultas incluírem variáveis ou entrada do usuário, pois ajuda a evitar ataques de injeção de SQL:

$status = 'publish';
$postType = 'post';
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT ID, post_title FROM {$wpdb->posts} 
        WHERE post_status = %s AND post_type = %s",
        $status,
        $postType
    )
);

Se você verificar false após qualquer operação no banco de dados, isso permitirá capturar erros:

$updated = $wpdb->update(
    $wpdb->posts,
    ['post_status' => 'draft'],
    ['ID' => 123],
    ['%s'],
    ['%d']
);

if ($updated === false) {
    $this->components->error('Database update failed');
    return 1;
}

Os tipos de post personalizados e taxonomias funcionam por meio das funções padrão do WordPress:

$terms = get_terms([
    'taxonomy' => 'category',
    'hide_empty' => false,
]);

foreach ($terms as $term) {
    wp_update_term($term->term_id, 'category', [
        'description' => 'Updated via command',
    ]);
}

Comandos WP-CLI personalizados são simples com Acorn e Artisan Console

O Acorn permite que você utilize as classes de comando estruturadas do Laravel, componentes de saída formatada, tratamento adequado de erros e muito mais, ao mesmo tempo em que oferece acesso completo às funções e aos dados do WordPress.

Você pode integrar comandos por meio de acesso SSH e agendamento com cron dentro da Kinsta. Você também pode adicionar comandos aos seus scripts de implantação para automatizar a manutenção, por exemplo, usando fluxos de trabalho do GitHub Actions.

Se você está pronto para centralizar suas tarefas de manutenção do WordPress com comandos WP-CLI personalizados, a hospedagem gerenciada para WordPress da Kinsta inclui acesso SSH e WP-CLI em todos os planos.

Joel Olawanle Kinsta

Joel é um desenvolvedor Frontend que trabalha na Kinsta como Editor Técnico. Ele é um professor apaixonado com amor pelo código aberto e já escreveu mais de 200 artigos técnicos, principalmente sobre JavaScript e seus frameworks.