Wahrscheinlich hast du PHP-Skripte, die verschiedene Aufgaben erledigen, z. B. verwaiste Beitrags-Metadaten aufräumen oder abgelaufene Transienten löschen. Mit der Zeit wird diese Sammlung immer größer und befindet sich in einer Themedatei, einem Plugin-Ordner oder einem versteckten Verzeichnis. Acorn hilft dabei, diese Unordnung in den Griff zu bekommen, indem es die Artisan Console von Laravel auf WordPress überträgt.

Das bedeutet, dass du eigene WP-CLI-Befehle mit strukturierten Klassendateien erstellen kannst, die deine Wartungslogik zentralisieren. Diese Befehle werden in der Entwicklung, im Staging und in der Produktion einheitlich ausgeführt und enthalten Fortschrittsanzeigen, formatierte Tabellenausgaben, eine angemessene Fehlerbehandlung und vieles mehr. Du kannst sie dann über SSH auslösen, mit Cron-Jobs planen oder während der Bereitstellung ausführen.

So installierst du Acorn und führst Befehle aus

Der erste Schritt besteht darin, die benötigten Abhängigkeiten zu installieren. Acorn benötigt PHP 8.2 oder höher, Composer zur Verwaltung der Abhängigkeiten und WP-CLI auf deinem Server. Bei Kinsta ist WP-CLI in allen Hosting-Paketen enthalten, sodass du sofort mit der Erstellung von Befehlen beginnen kannst.

Du installierst Acorn über den Composer, indem du composer require roots/acorn im Stammverzeichnis deines Projekts verwendest. Dann fügst du den Boot-Code entweder in die functions.php Datei deines Themes oder in deine Haupt-Plugin-Datei ein:

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

Das Zero-Config-Setup speichert den Anwendungscache und die Logs im WordPress-Cache-Verzeichnis unter [wp-content]/cache/acorn/, während deine Befehle im Verzeichnis app/ des Themes liegen.

Die traditionelle Struktur folgt den Laravel-Konventionen, z. B. mit eigenen Verzeichnissen für app/, config/, storage/ und resources/ im Stammverzeichnis deines Projekts. Du richtest dies mit einer Zeile ein:

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

Wenn du wp acorn list aufrufst, wird deine Installation überprüft, indem alle verfügbaren Acorn-Befehle angezeigt werden. Ab diesem Zeitpunkt werden alle benutzerdefinierten Befehle, die du erstellst, im Verzeichnis app/Console/Commands/ gespeichert. Acorn erkennt automatisch alle Befehlsklassen in diesem Verzeichnis und registriert sie bei WP-CLI.

Erstelle deinen ersten Artisan-Befehl

Der Artisan-Befehl wp acorn make:command CleanupCommand erstellt deine Datei mit der von dir benötigten Struktur. Sie enthält drei Schlüsselelemente, die jeder Artisan-Befehl braucht:

  • Eine $signature Eigenschaft, die deinen Befehlsnamen und deine Optionen definiert.
  • Die Eigenschaft $description für den Hilfetext.
  • Eine handle() Methode, die deine Befehlslogik enthält.

In diesem Fall baut sie app/Console/Commands/CleanupCommand.php mit einer grundlegenden Befehlsstruktur auf:

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

Die Eigenschaft $signature verwendet eine bestimmte Syntax:

  • Ein Basisbefehl braucht nur einen Namen, z. B. cleanup:run.
  • Du fügst erforderliche Argumente hinzu, indem du sie in geschweifte Klammern einschließt (z. B.cleanup:run {days}).
  • Optionale Argumente erhalten ein Fragezeichen: cleanup:run {days?}.
  • Optionen werden mit Doppelstrichen versehen: cleanup:run {--force} {--limit=100}.

Als Nächstes fügst du eine Beschreibung für den Befehl und deine grundlegende Logik ein:

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

Du kannst dies mit wp acorn cleanup:test --dry-run testen. Der Befehl gibt formatierte Nachrichten aus, die das Komponentensystem von Artisan verwenden. Die Methode $this->components->info() zeigt Erfolgsmeldungen in grün an. Du kannst auch $this->components->error() für Fehler, $this->components->warn() für Warnungen und $this->line() für reine Textausgaben verwenden.

Wie man 3 praktische Wartungsbefehle erstellt

Hier sind einige Beispiele, die dir helfen, Wartungsaufgaben für die Datenbank zu erledigen, die bei vielen WordPress-Websites anfallen.

Obwohl jeder Befehl eine Fehlerbehandlung enthält und weitgehend den WordPress-Codierungsstandards folgt, um deine Daten zu schützen, solltest du sie dennoch als Grundgerüst für deine eigenen Projekte verwenden, anstatt sie einfach zu kopieren und einzufügen.

1. Verwaiste Beitrags-Metadaten löschen

Post-Metadaten bleiben auch nach dem Löschen von Beiträgen erhalten. Das passiert, wenn Plugins die Löschfunktionen von WordPress umgehen und Metadateneinträge hinterlassen, die auf Beiträge verweisen, die nicht mehr existieren. Mit der Zeit verlangsamen diese Daten deine Datenbankabfragen.

Sobald du den Befehl mit wp acorn make:command CleanupOrphanedMeta erstellt hast, kannst du mit der Struktur und Signatur der Befehlsklasse beginnen:

<?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...');

Der Befehl verwendet eine LEFT JOIN Abfrage, um diese verwaisten Datensätze zu finden. Das Muster prüft auf NULL Werte in der Tabelle posts. Wenn die Verknüpfung NULL ergibt, gehören die Metadaten zu einem gelöschten Beitrag:

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

Wenn der Befehl verwaiste Beiträge findet, zeigt er dir die Anzahl der Einträge an und nimmt ein paar Stichproben, wenn du im Trockenlauf bist. Du musst überprüfen, was gelöscht wird, bevor du den Befehl bestätigst:

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

Die eigentliche Löschung erfolgt über $wpdb->prepare(), um SQL-Injection-Angriffe zu vermeiden. Der Befehl verarbeitet jeweils 1.000 Datensätze auf einmal, um Speicherprobleme auf Websites mit Millionen von Metadateneinträgen zu vermeiden:

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

Um den Befehl auszuführen, verwende wp acorn maintenance:cleanup-orphaned-meta --dry-run.

2. Abgelaufene Transienten löschen

WordPress speichert Transienten in wp_options mit Verfallsdaten. Ein täglicher Cron-Job bereinigt diese Daten, aber manchmal musst du sie während der Wartungsfenster oder wenn Transients zu einem Problem werden, manuell löschen.

Nachdem du den Befehl mit wp acorn make:command CleanupTransients erstellt hast, kannst du die Befehlsstruktur einrichten:

<?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...');

Diese Löschabfrage verwendet die Mehrtabellensyntax DELETE, um sowohl den Transienten als auch seine Timeout-Option auf einmal zu entfernen. Die Abfrage findet Timeout-Datensätze, bei denen der Verfallszeitstempel abgelaufen ist:

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

Überprüfe auch auf Fehler und verfolge die Anzahl der Löschungen:

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

        $transientCount = $deleted;

Bei Multisite-Installationen führt der Befehl eine zweite Abfrage für Site Transients durch. Diese verwenden unterschiedliche Tabellenpräfixe, folgen aber demselben Löschmuster:

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

Um den Befehl auszuführen, rufe wp acorn maintenance:cleanup-transients auf.

3. Überprüfen von automatisch geladenen Optionen

Autoload-Optionen werden bei jeder Anfrage geladen, die deine WordPress-Website bearbeitet. Wenn diese Daten eine Größe von 1 MB überschreiten, kann es zu Verlangsamungen und Speicherverbrauchsspitzen kommen. Mit diesem Befehl findest du die größten automatisch geladenen Optionen, damit du herausfinden kannst, welche Plugins die Ursache für die Überlastung sind.

Zuerst erstellst du den Audit-Befehl mit wp acorn make:command AuditAutoload. Dann definierst du die Befehlssignatur mit einem konfigurierbaren Schwellenwert:

<?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...');

Berechne von hier aus die Gesamtgröße aller automatisch geladenen Optionen:

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

Der Befehl führt eine Abfrage nach Optionen durch, die über deinem Schwellenwert liegen, sortiert sie nach Größe und beschränkt die Ergebnisse auf die größten 20:

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

Die Artisan-Methode $this->table() formatiert diese Ergebnisse als ASCII-Tabelle: Das Lesen der tabellarischen Daten in deinem Terminal schlägt das Parsen der rohen Abfrageausgabe:

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

Der Befehl gibt eine Warnung aus, wenn der gesamte Autoload 3 MB überschreitet, was auf ein Leistungsproblem hinweist, das du beheben musst:

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

        return 0;
    }
}

Um das Audit auszuführen, verwende wp acorn maintenance:audit-autoload --threshold=500000.

Wie man innerhalb von Befehlen auf WordPress-Daten zugreift

WordPress-Funktionen funktionieren innerhalb deiner Befehlsmethoden, weil Acorn innerhalb des Lebenszyklus von WordPress bootet. Das bedeutet, dass du einige Funktionen wie get_posts() oder get_option() ohne besondere Einrichtung aufrufen kannst:

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

Für direkte Datenbankabfragen deklarierst du die globale $wpdb am Anfang deiner Methode:

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() dies ist ideal, wenn deine Abfragen Variablen oder Benutzereingaben enthalten, da es hilft, SQL-Injection-Angriffe zu verhindern:

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

Wenn du nach jeder Datenbankoperation auf false testest, solltest du Fehler erkennen können:

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

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

Benutzerdefinierte Beitragstypen und Taxonomien funktionieren über die Standardfunktionen von WordPress:

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

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

Benutzerdefinierte WP-CLI-Befehle sind mit Acorn und Artisan Console ganz einfach

Mit Acorn kannst du auf die strukturierten Befehlsklassen von Laravel, formatierte Ausgabekomponenten, eine angemessene Fehlerbehandlung und vieles mehr zugreifen, während du gleichzeitig vollen Zugriff auf die Funktionen und Daten von WordPress hast.

Du kannst Befehle über SSH-Zugang und Cron-Planung in Kinsta integrieren. Du kannst auch Befehle zu deinen Bereitstellungs-Skripten hinzufügen, um die Wartung zu automatisieren, zum Beispiel mit GitHub Actions Workflows.

Wenn du deine WordPress-Wartungsaufgaben mit benutzerdefinierten WP-CLI-Befehlen zentralisieren möchtest, sind SSH-Zugang und WP-CLI in allen Managed WordPress Hosting-Tarifen von Kinsta enthalten.

Joel Olawanle Kinsta

Joel ist Frontend-Entwickler und arbeitet bei Kinsta als Technical Editor. Er ist ein leidenschaftlicher Lehrer mit einer Vorliebe für Open Source und hat über 200 technische Artikel geschrieben, die sich hauptsächlich um JavaScript und seine Frameworks drehen.