Plugin & Theme Update Validation

Overview

Extensions can participate in the update pipeline by validating whether an update is safe to apply. Validation runs during the extension verify stage (validate) and during core verify via extension_compat.

ValidatesUpdate interface

Implement on src/Plugin.php or src/Theme.php:

use App\Update\Contracts\ValidatesUpdate;
use App\Update\Pipeline\UpdateContext;
use App\Update\Reports\CheckResult;
use App\Update\Reports\StageReport;

class Plugin implements ValidatesUpdate
{
    public function validateUpdate(UpdateContext $context): StageReport
    {
        return new StageReport(
            stage: 'my_plugin_validation',
            status: StageReport::PASS,
            label: 'My Plugin',
            checks: [
                CheckResult::pass('tables_ready', 'Required tables exist'),
            ],
            message: 'Safe to update.',
        );
    }
}

Return StageReport::WARN to allow proceed-with-caution, or StageReport::FAIL to block the update.

Filter-based validation

Register without a class:

add_filter('valpress_update_validate_my-plugin', function ($report, $context, $type) {
    if (!Schema::hasTable('my_plugin_queue')) {
        return new StageReport(
            stage: 'my_plugin_validation',
            status: StageReport::FAIL,
            label: 'My Plugin',
            checks: [
                CheckResult::fail('queue_table', 'Queue table missing'),
            ],
            message: 'Run migrations before updating.',
            actions: [
                \App\Update\Reports\RepairAction::repair('plugin_migrate', 'Run plugin migrations', ['slug' => 'my-plugin']),
            ],
        );
    }

    return $report;
}, 10, 3);

Scaffold commands

php artisan vp:make-plugin-validation my-plugin
php artisan vp:make-theme-validation my-theme
php artisan vp:make-extension-composer plugin my-plugin
php artisan vp:make-extension-composer theme my-theme

Uses the vp: prefix so ValPress commands stay separate from Laravel's built-in make:* generators.

  • vp:make-plugin-validation creates public/plugins/{slug}/src/Plugin.php implementing ValidatesUpdate.
  • vp:make-theme-validation creates public/themes/{slug}/src/Theme.php implementing ValidatesUpdate.
  • vp:make-extension-composer adds a composer.require stub to an existing extension config.php.

Reference example

The pattern below checks for a required migration table, attaches repair actions on failure, and warns on self-update. Scaffold with vp:make-plugin-validation, then adapt this logic in src/Plugin.php:

<?php

namespace Plugins\MyPlugin;

use App\Update\Contracts\ValidatesUpdate;
use App\Update\Pipeline\UpdateContext;
use App\Update\Reports\CheckResult;
use App\Update\Reports\RepairAction;
use App\Update\Reports\StageReport;
use Illuminate\Support\Facades\Schema;

class Plugin implements ValidatesUpdate
{
    public function validateUpdate(UpdateContext $context): StageReport
    {
        $checks = [];

        if (!Schema::hasTable('my_plugin_log')) {
            $checks[] = CheckResult::fail(
                'log_table',
                'Log table is missing. Run plugin migrations before updating.',
            );

            return new StageReport(
                stage: 'my_plugin_validation',
                status: StageReport::FAIL,
                label: 'My Plugin',
                checks: $checks,
                actions: [
                    RepairAction::repair('plugin_migrate', 'Run plugin migrations', ['slug' => 'my-plugin']),
                    RepairAction::route('Open Plugins', 'admin.plugins.index'),
                ],
                message: 'Plugin database schema is not ready for an update.',
            );
        }

        $checks[] = CheckResult::pass('log_table', 'Log table exists.');

        if ($context->isExtension() && $context->slug === 'my-plugin') {
            $checks[] = CheckResult::warn(
                'self_update',
                'Self-update detected — review release notes before applying.',
            );

            return new StageReport(
                stage: 'my_plugin_validation',
                status: StageReport::WARN,
                label: 'My Plugin',
                checks: $checks,
                message: 'Safe to update with warnings.',
            );
        }

        return new StageReport(
            stage: 'my_plugin_validation',
            status: StageReport::PASS,
            label: 'My Plugin',
            checks: $checks,
            message: 'No blocking issues found.',
        );
    }
}

A matching migration might look like:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        if (Schema::hasTable('my_plugin_log')) {
            return;
        }

        Schema::create('my_plugin_log', function (Blueprint $table) {
            $table->id();
            $table->string('event');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('my_plugin_log');
    }
};

Place it under public/plugins/my-plugin/database/migrations/ so activation and the plugin_migrate repair can run it.

CMS contributors can also install the automated test fixture from a ValPress CMS checkout:

cp -r tests/fixtures/plugins/valpress-update-example public/plugins/valpress-update-example

On Windows (PowerShell):

Copy-Item -Recurse tests\fixtures\plugins\valpress-update-example public\plugins\valpress-update-example

Core update participation

When verifying a core update, ValPress runs validators for:

  • All active plugins
  • Active theme(s), including child + parent

Critical extension failures block verification. Composer constraint conflicts against the upcoming core lock also block verification; see Extension Composer Dependencies.

Extension self-update

When updating a plugin or theme through the Update Center, only the targeted extension is validated via validateTarget().

Related docs