Waarschijnlijk heb je inmiddels een reeks losse PHP-scripts die specifieke taken uitvoeren, zoals het opschonen van oude berichtmetadata of het verwijderen van verlopen transients. In de praktijk groeit die verzameling langzaam maar zeker en raakt ze verspreid over je thema, een pluginmap of een obscure submap waar niemand nog naar omkijkt.

Dit betekent dat je custom WP-CLI-commando’s kunt bouwen met gestructureerde klassebestanden waarin je onderhoudslogica centraal is ondergebracht. Deze commando’s worden consistent uitgevoerd in ontwikkeling, staging en productie, inclusief voortgangsindicatoren, geformatteerde tabeluitvoer, degelijke foutafhandeling en meer. Je kunt ze vervolgens activeren via SSH, plannen met cronjobs of uitvoeren als onderdeel van je deploymentproces.

Acorn installeren en commando’s uitvoeren

De eerste stap is het installeren van de dependencies die je nodig hebt. Acorn heeft PHP 8.2 of hoger, Composer voor het beheren van dependencies en WP-CLI op je server nodig. Kinsta levert WP-CLI bij alle hostingpakketten, dus je kunt meteen beginnen met het bouwen van commando’s.

Je installeert Acorn via Composer met composer require roots/acorn in de project root. Voeg vervolgens de opstartcode toe aan het bestand functions.php van je thema of aan je hoofdpluginbestand:

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

De zero-config setup slaat de applicatiecache en logs op in de WordPress cache directory op [wp-content]/cache/acorn/, terwijl je commando’s in de app/ map van je thema staan.

De traditionele structuur volgt de Laravel conventies, zoals speciale mappen voor app/, config/, storage/, en resources/ in je project root. Je stelt dit in met één regel:

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

Als je wp acorn list uitvoert, controleert dit je installatie door alle beschikbare Acorn commando’s weer te geven. Vanaf dit punt worden alle custom commando’s die je maakt opgeslagen in de map app/Console/Commands/. Acorn ontdekt automatisch alle commandoklassen op deze locatie en registreert ze bij WP-CLI.

Je eerste Artisan commando maken

Het wp acorn make:command CleanupCommand Artisan commando genereert je bestand met de structuur die je nodig hebt. Het bevat drie belangrijke elementen die elk Artisan commando nodig heeft:

  • Een $signature property die je opdrachtnaam en opties definieert.
  • De$description property voor helptekst.
  • Een handle() methode waar de logica van je commando in staat.

In dit geval bouwt het app/Console/Commands/CleanupCommand.php met een basiscommandostructuur:

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

De property $signature gebruikt een specifieke syntaxis:

  • Een basisopdracht heeft alleen een naam nodig, zoals cleanup:run.
  • Je voegt verplichte argumenten toe door ze tussen accolades te zetten (cleanup:run {days}, als voorbeeld).
  • Optionele argumenten krijgen een vraagteken: cleanup:run {days?}.
  • Opties gebruiken dubbele streepjes: cleanup:run {--force} {--limit=100}.

Voeg vervolgens een beschrijving van het commando en je basislogica toe:

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

Je kunt dit testen met wp acorn cleanup:test --dry-run. De opdracht voert geformatteerde berichten uit met behulp van het componentsysteem van Artisan. De methode $this->components->info() geeft succesberichten in het groen weer. Je kunt ook $this->components->error() gebruiken voor fouten, $this->components->warn() voor waarschuwingen en $this->line() voor uitvoer in platte tekst.

Zo bouw je 3 praktische onderhoudsopdrachten

Hier zijn enkele voorbeelden die je helpen om database onderhoudstaken aan te pakken die op veel WordPress websites voorkomen.

Hoewel elke opdracht foutafhandeling bevat en grotendeels de WordPress coderingsstandaarden volgt om je gegevens veilig te houden, zou je deze nog steeds moeten gebruiken als een framework voor je eigen projecten in plaats van ze simpelweg te copy-pasten.

1. Oude bericht metadata opschonen

Metadata over berichten blijven bestaan nadat je berichten hebt verwijderd. Dit gebeurt wanneer plugins de verwijderhooks van WordPress omzeilen en metadata-items achterlaten die verwijzen naar berichten die niet meer bestaan. Na verloop van tijd vertraagt deze ophoping je database queries.

Zodra je het commando hebt gemaakt met wp acorn make:command CleanupOrphanedMeta, kun je beginnen met de structuur en de handtekening van de opdrachtklasse:

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

De opdracht gebruikt een LEFT JOIN query om deze verweesde records te vinden. Het patroon controleert op NULL waarden in de tabel met berichten. Als de join NULL teruggeeft, behoren de metadata tot een verwijderd bericht:

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

Als het commando oude (orphaned) records vindt, laat het je een telling zien en samples van een paar records als je in de testmodus zit. Je moet controleren wat er wordt verwijderd voordat je het vastlegt:

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

De daadwerkelijke verwijdering maakt gebruik van $wpdb->prepare() om SQL injectie aanvallen te voorkomen. Het commando verwerkt 1000 records per keer, wat geheugenproblemen voorkomt op sites met miljoenen metadata entries:

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

Gebruik wp acorn maintenance:cleanup-orphaned-meta --dry-run om de opdracht uit te voeren.

2. Verlopen transients verwijderen

WordPress slaat transients op in wp_options met vervaldtijd. Hoewel een dagelijkse cronjob deze opruimt, moet je soms een handmatige schoonmaak uitvoeren tijdens onderhoudsvensters of wanneer tijdelijke opgeblazenheid een probleem wordt.

Nadat je de opdracht met wp acorn make:command CleanupTransients hebt gegenereerd, kun je de opdrachtstructuur instellen:

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

Deze verwijderquery gebruikt de syntaxis voor meerdere tabellen DELETE om zowel de transiënt als zijn time-outoptie in één keer te verwijderen. De query vindt time-outrecords waarvan het vervaltijdstempel is verstreken:

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

Controleer ook op fouten en houd het aantal verwijderingen bij:

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

        $transientCount = $deleted;

Op Multisite installaties voert het commando een tweede query uit voor site transients. Deze gebruiken verschillende tabel prefixes, maar volgen hetzelfde verwijderingspatroon:

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

Om de opdracht uit te voeren, voer je wp acorn maintenance:cleanup-transients uit.

3. Autoloaded options controleren

Autoloaded options worden geladen bij elke aanvraag die je WordPress site verwerkt. Je kunt vertragingen en pieken in het geheugengebruik zien wanneer deze gegevens de 1MB overschrijden. Dit commando vindt de grootste autoloaded options, zodat je kunt achterhalen welke plugins de bloat veroorzaken.

Maak eerst het audit commando met wp acorn make:command AuditAutoload. Definieer vervolgens de commando handtekening met een configureerbare drempelwaarde:

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

Bereken vanaf hier de totale grootte van alle autoloaded options:

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

Het commando voert een query uit voor options boven je drempel, sorteert ze op grootte en beperkt de resultaten tot de grootste 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;
        }

Artisan’s $this->table() methode formatteert deze resultaten als een ASCII tabel: het lezen van gegevens in tabelvorm in je terminal verslaat het parsen van de ruwe query uitvoer:

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

Het commando geeft een waarschuwing als de totale autoload 3MB overschrijdt, wat duidt op een prestatieprobleem dat je moet aanpakken:

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

        return 0;
    }
}

Gebruik wp acorn maintenance:audit-autoload --threshold=500000 om de audit uit te voeren.

Toegang tot WordPress gegevens binnen commando’s

WordPress functies werken binnen je opdrachtmethodes omdat Acorn opstart binnen de levenscyclus van WordPress. Dit betekent dat je sommige functies zoals get_posts() of get_option() kunt callen zonder speciale instellingen:

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

Voor directe database query’s declareer je de $wpdb global aan het begin van je 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() is ideaal wanneer je query’s variabelen of gebruikersinvoer bevatten, omdat het SQL injectieaanvallen helpt voorkomen:

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

Als je test op false na elke databasebewerking, zou dit je fouten moeten laten opmerken:

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

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

Custom berichttypen en taxonomieën werken via standaard WordPress functies:

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

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

Custom WP-CLI commando’s zijn eenvoudig met Acorn en Artisan Console

Met Acorn heb je toegang tot de gestructureerde opdrachtklassen van Laravel, opgemaakte uitvoercomponenten, goede foutafhandeling en meer, terwijl je volledige toegang hebt tot de functies en gegevens van WordPress.

Je kunt commando’s integreren via SSH toegang en cron planning binnen Kinsta. Je kunt ook commando’s toevoegen aan je implementatiescripts om onderhoud te automatiseren, bijvoorbeeld door GitHub Actions workflows te gebruiken.

Als je klaar bent om je WordPress onderhoudstaken te centraliseren met aangepaste WP-CLI commando’s, dan bevat Kinsta’s managed WordPress hosting SSH toegang en WP-CLI op alle plannen.

Joel Olawanle Kinsta

Joel is een Frontend developer die bij Kinsta werkt als Technical Editor. Hij is een gepassioneerd leraar met liefde voor open source en heeft meer dan 200 technische artikelen geschreven, voornamelijk over JavaScript en zijn frameworks.