Probabilmente hai degli script PHP che svolgono varie attività, come la pulizia dei metadati dei post orfani o l’eliminazione dei transienti scaduti. Nel corso del tempo, questa collezione cresce e vive in un file di tema, in una cartella di plugin o in una directory nascosta. Acorn ci aiuta a controllare questa disorganizzazione portando la Artisan Console di Laravel su WordPress.

Ciò significa che puoi creare comandi WP-CLI personalizzati con file di classe strutturati che centralizzano la tua logica di manutenzione. Questi comandi vengono eseguiti in modo coerente tra sviluppo, staging e produzione utilizzando indicatori di avanzamento, output di tabelle formattate, gestione corretta degli errori e altro ancora. Puoi quindi attivarli tramite SSH, programmarli con cron job o eseguirli durante le distribuzioni.

Come installare Acorn ed eseguire i comandi

Il primo passo è installare le dipendenze necessarie. Acorn necessita di PHP 8.2 o di una versione superiore, Composer per la gestione delle dipendenze e WP-CLI in esecuzione sul server. Kinsta include WP-CLI su tutti i piani di hosting, quindi puoi iniziare subito a creare comandi.

Installa Acorn tramite Composer utilizzando composer require roots/acorn nella root del progetto. Poi aggiungi il codice di avvio al file functions.php del tuo tema o al file principale del plugin:

<?php
use RootsAcornApplication;
if (! class_exists(RootsAcornApplication::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([
            AppProvidersThemeServiceProvider::class,
        ])
        ->boot();
}, 0);

La configurazione zero-config memorizza la cache dell’applicazione e i log nella directory della cache di WordPress all’indirizzo [wp-content]/cache/acorn/, mentre i tuoi comandi si trovano nella directory app/ del tema.

La struttura tradizionale segue le convenzioni di Laravel, come le directory dedicate a app/, config/, storage/ e resources/ nella root del progetto. Per configurare questa struttura basta una riga:

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

Se esegui wp acorn list, questo verifica la tua installazione visualizzando tutti i comandi Acorn disponibili. Da questo momento, tutti i comandi personalizzati che crei sono memorizzati nella cartella app/Console/Commands/. Acorn scopre automaticamente tutte le classi di comandi presenti in questa posizione e le registra con WP-CLI.

Creare il primo comando Artisan

Il comando Artisan wp acorn make:command CleanupCommand genera il file con la struttura che ti servirà. Contiene tre elementi chiave di cui ogni comando Artisan ha bisogno:

  • Una proprietà $signature che definisce il nome del comando e le opzioni.
  • La proprietà $description per il testo di aiuto.
  • Un metodo handle() dove risiede la logica del comando.

In questo caso, costruisce app/Console/Commands/CleanupCommand.php con una struttura di comando di base:

<?php
namespace AppConsoleCommands;
use IlluminateConsoleCommand;
class CleanupCommand extends Command
{
    protected $signature = 'app:cleanup';
    protected $description = 'Command description';
    public function handle()
    {
        //
    }
}

La proprietà $signature utilizza una sintassi specifica:

  • Un comando di base ha bisogno solo di un nome, come cleanup:run.
  • Gli argomenti obbligatori si aggiungono avvolgendoli tra parentesi graffe (cleanup:run {days}, ad esempio).
  • Gli argomenti facoltativi hanno un punto interrogativo: cleanup:run {days?}.
  • Le opzioni utilizzano doppi trattini: cleanup:run {--force} {--limit=100}.

Quindi, includi una descrizione del comando e la tua logica di base:

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;
}

Puoi verificarlo utilizzando wp acorn cleanup:test --dry-run. Il comando produce messaggi formattati utilizzando il sistema di componenti di Artisan. Il metodo $this->components->info() mostra i messaggi di successo in verde. Puoi anche usare $this->components->error() per gli errori, $this->components->warn() per gli avvertimenti e $this->line() per l’output in testo normale.

Come creare 3 pratici comandi di manutenzione

Ecco alcuni esempi che aiutano ad affrontare le attività di manutenzione del database che si presentano in molti siti web WordPress.

Sebbene ogni comando includa la gestione degli errori e segua in larga misura gli standard di codifica di WordPress per mantenere i tuoi dati al sicuro, dovresti comunque utilizzarli come scheletro per i tuoi progetti piuttosto che fare un semplice copia-incolla.

1. Pulire i metadati dei post orfani

I metadati dei post rimangono anche dopo la loro cancellazione. Questo accade quando i plugin aggirano gli hook di cancellazione di WordPress e lasciano voci di metadati che puntano a post che non esistono più. Con il tempo, questo ingombro rallenta le query del database.

Una volta creato il comando con wp acorn make:command CleanupOrphanedMeta, puoi iniziare con la struttura e la firma della classe del comando:

<?php
namespace AppConsoleCommands;
use IlluminateConsoleCommand;
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...');

Il comando utilizza una query LEFT JOIN per trovare questi record orfani. Lo schema verifica la presenza di valori NULL nella tabella dei post. Se il join restituisce NULL, i metadati appartengono a un post cancellato:

        // 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 il comando trova i post orfani, mostra un conteggio e campiona alcuni record se sei in modalità dry-run. Dovrai verificare cosa viene cancellato prima di eseguire il commit:

        $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;
        }

L’eliminazione effettiva utilizza $wpdb->prepare() per evitare attacchi SQL injection. Il comando elabora 1.000 record alla volta, evitando così problemi di memoria in siti con milioni di voci di metadati:

        // 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;
    }
}

Per eseguire il comando, usa wp acorn maintenance:cleanup-orphaned-meta --dry-run.

2. Eliminare i transient scaduti

WordPress memorizza i transient /a> in wp_options con date di scadenza. Mentre un cron job giornaliero li ripulisce, a volte è necessario eseguire una pulizia manuale durante le finestre di manutenzione o quando il gonfiore transient diventa un problema.

Dopo aver generato il comando con wp acorn make:command CleanupTransients, puoi impostare la struttura del comando:

<?php
namespace AppConsoleCommands;
use IlluminateConsoleCommand;
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...');

Questa query di cancellazione utilizza la sintassi multi-tabella DELETE per rimuovere sia il transient che la sua opzione di timeout in una sola volta. La query trova i record di timeout in cui il timestamp di scadenza è passato:

        // 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()
            )
        );

Controlla anche gli errori e tiene traccia del conteggio delle eliminazioni:

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

        $transientCount = $deleted;

Nelle installazioni multisito, il comando esegue una seconda query per i transient dei siti. Queste utilizzano prefissi di tabella diversi ma seguono lo stesso schema di cancellazione:

        // 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;
    }
}

Per eseguire il comando, lancia wp acorn maintenance:cleanup-transients.

3. Controllare le opzioni autocaricate

Le opzioni autocaricate vengono caricate ad ogni richiesta che il tuo sito WordPress gestisce. Inizierai a notare rallentamenti e picchi di consumo della memoria quando questi dati superano 1MB. Questo comando individua le opzioni più caricate in modo da poter individuare i plugin che causano il gonfiore.

Per prima cosa, crea il comando di verifica con wp acorn make:command AuditAutoload. Poi, definisci la firma del comando con una soglia configurabile:

<?php
namespace AppConsoleCommands;
use IlluminateConsoleCommand;
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...');

Da qui, calcola la dimensione totale di tutte le opzioni caricate automaticamente:

        // 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)");

Il comando esegue una query per le opzioni superiori alla tua soglia, le ordina per dimensione e limita i risultati alle 20 più grandi:

        // 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;
        }

Il metodo $this->table() di Artisan formatta questi risultati come una tabella ASCII: leggere i dati tabellari nel tuo terminale significa analizzare l’output grezzo della query:

        $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
        );

Il comando lancia un avviso quando il carico automatico totale supera i 3 MB, il che indica un problema di prestazioni da risolvere:

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

        return 0;
    }
}

Per eseguire la verifica, usa wp acorn maintenance:audit-autoload --threshold=500000.

Come accedere ai dati di WordPress all’interno dei comandi

Le funzioni di WordPress funzionano all’interno dei tuoi metodi di comando perché Acorn si avvia all’interno del ciclo di vita di WordPress. Questo significa che puoi chiamare alcune funzioni come get_posts() o get_option() senza alcuna configurazione speciale:

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;
}

Per le interrogazioni dirette al database, dichiara il globale $wpdb all’inizio del metodo:

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() è ideale quando le tue query includono variabili o input dell’utente, in quanto aiuta a prevenire gli attacchi di SQL injection:

$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 verifichi la presenza di false dopo ogni operazione sul database, potrai individuare gli errori:

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

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

I tipi di post e le tassonomie personalizzate funzionano attraverso le funzioni standard di WordPress:

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

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

I comandi WP-CLI personalizzati sono semplici da usare con Acorn e Artisan Console

Acorn permette di accedere alle classi di comandi strutturate di Laravel, ai componenti di output formattati, alla corretta gestione degli errori e molto altro ancora, pur dandoti pieno accesso alle funzioni e ai dati di WordPress.

Puoi integrare i comandi attraverso l’accesso SSH e la programmazione cron all’interno di Kinsta. Puoi anche aggiungere comandi ai tuoi script di distribuzione per automatizzare la manutenzione, ad esempio utilizzando i flussi di lavoro di GitHub Actions.

Se non vedi l’ora di centralizzare le attività di manutenzione di WordPress con comandi WP-CLI personalizzati, l’hosting WordPress gestito di Kinsta include l’accesso SSH e WP-CLI su tutti i piani.

Joel Olawanle Kinsta

Joel è uno Frontend developer che lavora in Kinsta come redattore tecnico. È un insegnante appassionato che ama l'open source e ha scritto oltre 200 articoli tecnici principalmente su JavaScript e i suoi framework.