Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f082a71
Add new hooks for runtime setup and shutdown
ernilambar May 4, 2026
1ac3d00
Add new hooks for runtime setup and shutdown
ernilambar May 4, 2026
c461477
Add setup/cleanup actions to Runtime_Environment_Setup
kagg-design Apr 23, 2026
d2a029b
Introduce WP_PLUGIN_CHECK_BOOTSTRAP_FILE extension point
kagg-design Apr 23, 2026
eb521e3
Add tests for setup/cleanup hooks and bootstrap file mechanism
kagg-design Apr 23, 2026
4fbe0d6
Document setup/cleanup hooks and bootstrap file in changelog
kagg-design Apr 23, 2026
c235214
Add early-return PHPUnit test and Behat hook scenario
kagg-design May 25, 2026
af6550a
Fix action names.
kagg-design May 26, 2026
d761446
Add wp_plugin_check_phpcs_args filter
kagg-design May 26, 2026
a99e272
Update version.
kagg-design May 26, 2026
3c0b3ab
Add wp_plugin_check_check_result filter
kagg-design May 26, 2026
bf21297
Code style.
kagg-design May 26, 2026
b3d0e28
Fix Behat fixtures for early WP-CLI load order and post-merge hook names
kagg-design May 26, 2026
e84a83d
Merge remote-tracking branch 'origin/1269-new-hooks' into 1269-new-hooks
kagg-design May 26, 2026
002391c
Activate the test plugin so runtime hooks actually fire
kagg-design May 26, 2026
2ccdead
Suppress PHPMD warnings and align PHPUnit hook arg assertions
kagg-design May 26, 2026
56383f1
Merge branch 'trunk' into 1269-new-hooks
kagg-design May 27, 2026
91f77c3
Merge branch 'trunk' into 1269-new-hooks
davidperezgar May 28, 2026
48fef28
Merge branch 'trunk' into 1269-new-hooks
kagg-design Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@
require_once __DIR__ . '/vendor/autoload.php';
}

/*
* Load the consumer-supplied bootstrap file (if configured) before the command is registered.
* This mirrors the behaviour of the `object-cache.php` drop-in so that integrations have one
* consistent extension point across admin and WP-CLI paths.
*
* The constant is expected to hold an absolute filesystem path. When the constant is defined
* but the file is missing, a WP-CLI warning is emitted and execution continues.
*/
if ( defined( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE' ) ) {
$plugin_check_bootstrap_file = WP_PLUGIN_CHECK_BOOTSTRAP_FILE;
if ( is_string( $plugin_check_bootstrap_file ) && '' !== $plugin_check_bootstrap_file ) {
if ( is_file( $plugin_check_bootstrap_file ) ) {
require_once $plugin_check_bootstrap_file;
} else {
WP_CLI::warning(
sprintf(
'Plugin Check: WP_PLUGIN_CHECK_BOOTSTRAP_FILE points to "%s", but the file does not exist.',
$plugin_check_bootstrap_file
)
);
}
}
unset( $plugin_check_bootstrap_file );
}

if ( ! isset( $context ) ) {
$context = new Plugin_Context( __DIR__ . '/plugin.php' );
}
Expand Down
26 changes: 26 additions & 0 deletions drop-ins/object-cache.copy.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ function plugin_check_initialize_runner() {

require_once $plugin_dir . 'vendor/autoload.php';

/*
* Load the consumer-supplied bootstrap file (if configured) before the runner is initialized.
* The bootstrap file is the earliest point at which an integration can register listeners for
* plugin-check actions/filters, since this drop-in runs before mu-plugins.
*
* The constant is expected to hold an absolute filesystem path. When the constant is defined
* but the file is missing, a PHP warning is emitted so that misconfiguration is visible; PCP
* keeps running regardless.
*/
if ( defined( 'WP_PLUGIN_CHECK_BOOTSTRAP_FILE' ) ) {
$plugin_check_bootstrap_file = WP_PLUGIN_CHECK_BOOTSTRAP_FILE;
if ( is_string( $plugin_check_bootstrap_file ) && '' !== $plugin_check_bootstrap_file ) {
if ( is_file( $plugin_check_bootstrap_file ) ) {
require_once $plugin_check_bootstrap_file;
} else {
trigger_error(
sprintf(
'Plugin Check: WP_PLUGIN_CHECK_BOOTSTRAP_FILE points to "%s", but the file does not exist.',
$plugin_check_bootstrap_file
),
E_USER_WARNING
);
}
}
}

if ( class_exists( 'WordPress\Plugin_Check\Utilities\Plugin_Request_Utility' ) ) {
// Initialize the Check Runner class based on the request.
WordPress\Plugin_Check\Utilities\Plugin_Request_Utility::initialize_runner();
Expand Down
34 changes: 31 additions & 3 deletions includes/Checker/Check_Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ public function plugin() {
/**
* Adds an error or warning to the respective stack.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
*
* @since 1.0.0
*
* @param bool $error Whether it is an error message.
Expand Down Expand Up @@ -130,9 +132,35 @@ public function add_message( $error, $message, $args = array() ) {
array_intersect_key( $args, $defaults )
);

$file = str_replace( $this->plugin()->path( '/' ), '', $data['file'] );
$line = $data['line'];
$column = $data['column'];
// Normalize the file path before the filter so consumers see the same value as the stored entry.
$data['file'] = str_replace( $this->plugin()->path(), '', $data['file'] );

/**
* Filters a single check result entry before it is recorded.
*
* Return `null` (or any non-array value) to suppress the entry entirely.
* Return a modified array to record the modified entry instead.
* The `$is_error` argument continues to drive whether the entry is stored
* as an error or a warning regardless of changes made to the filtered array —
* promotion / demotion is intentionally out of scope here.
*
* @since 2.0.0
*
* @param array|null $data Entry data with keys
* `message`, `code`, `file`, `line`, `column`, `link`, `docs`, `severity`.
* Return `null` to drop the entry.
* @param Check_Result $result The check result the entry will be added to.
* @param bool $is_error True if the entry is being recorded as an error, false if as a warning.
*/
$data = apply_filters( 'wp_plugin_check_check_result', $data, $this, $error );

if ( ! is_array( $data ) ) {
return;
}

$file = isset( $data['file'] ) ? (string) $data['file'] : '';
$line = isset( $data['line'] ) ? (int) $data['line'] : 0;
$column = isset( $data['column'] ) ? (int) $data['column'] : 0;
unset( $data['line'], $data['column'], $data['file'] );

if ( $error ) {
Expand Down
17 changes: 17 additions & 0 deletions includes/Checker/Checks/Abstract_PHP_CodeSniffer_Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ abstract protected function get_args( Check_Result $result );
* Amends the given result by running the check on the associated plugin.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*
* @since 1.0.0
*
Expand Down Expand Up @@ -92,6 +93,22 @@ final public function run( Check_Result $result ) {

$args = $this->get_args( $result );

/**
* Filters the PHPCS arguments for a check before it runs.
*
* Lets integrations override the PHPCS standard, runtime-set values,
* extensions, installed_paths, sniffs and other arguments returned by
* the check's `get_args()` implementation — without having to subclass
* and swap the check via `wp_plugin_check_checks`.
*
* @since 2.0.0
*
* @param array $args PHPCS arguments returned by `get_args()`.
* @param Abstract_PHP_CodeSniffer_Check $check The check instance.
* @param Check_Result $result The check result.
*/
$args = apply_filters( 'wp_plugin_check_phpcs_args', $args, $this, $result );

// Reset PHP_CodeSniffer config.
$this->reset_php_codesniffer_config();

Expand Down
214 changes: 136 additions & 78 deletions includes/Checker/Runtime_Environment_Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,73 +24,103 @@ final class Runtime_Environment_Setup {
*
* @global wpdb $wpdb WordPress database abstraction object.
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function set_up() {
global $wpdb, $wp_filesystem;

require_once ABSPATH . '/wp-admin/includes/upgrade.php';

// Get the existing site URL.
$site_url = get_option( 'siteurl' );

// Get the existing active plugins.
$active_plugins = get_option( 'active_plugins' );

// Get the existing active theme.
$active_theme = get_option( 'stylesheet' );

// Get the existing permalink structure.
$permalink_structure = get_option( 'permalink_structure' );

// Set the new prefix.
$prefix_cleanup = $this->amend_db_base_prefix();

// Create and populate the test database tables if they do not exist.
if ( $wpdb->posts !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->posts ) ) ) {
/*
* Set the same permalink structure *before* install finishes,
* so that wp_install_maybe_enable_pretty_permalinks() does not flush rewrite rules.
*
* See https://github.com/WordPress/plugin-check/issues/330
*/
add_action(
'populate_options',
static function () use ( $permalink_structure ) {
/*
* If pretty permalinks are not used, temporarily enable them by setting a permalink structure, to
* avoid flushing rewrite rules in wp_install_maybe_enable_pretty_permalinks().
* Afterwards, on the 'wp_install' action, set the original (empty) permalink structure.
*/
if ( ! $permalink_structure ) {
add_action(
'wp_install',
static function () use ( $permalink_structure ) {
update_option( 'permalink_structure', $permalink_structure );
}
);
$permalink_structure = '/%postname%/';
/**
* Fires before the runtime environment is set up.
*
* @since 2.0.0
*
* @param array $context {
* Context for the hook.
*
* @type bool $early_exit Whether the method exited before completing all setup steps.
* }
*/
do_action( 'wp_plugin_check_before_runtime_setup', array( 'early_exit' => false ) );

try {
require_once ABSPATH . '/wp-admin/includes/upgrade.php';

// Get the existing site URL.
$site_url = get_option( 'siteurl' );

// Get the existing active plugins.
$active_plugins = get_option( 'active_plugins' );

// Get the existing active theme.
$active_theme = get_option( 'stylesheet' );

// Get the existing permalink structure.
$permalink_structure = get_option( 'permalink_structure' );

// Set the new prefix.
$prefix_cleanup = $this->amend_db_base_prefix();

// Create and populate the test database tables if they do not exist.
if ( $wpdb->posts !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->posts ) ) ) {
/*
* Set the same permalink structure *before* install finishes,
* so that wp_install_maybe_enable_pretty_permalinks() does not flush rewrite rules.
*
* See https://github.com/WordPress/plugin-check/issues/330
*/
add_action(
'populate_options',
static function () use ( $permalink_structure ) {
/*
* If pretty permalinks are not used, temporarily enable them by setting a permalink structure, to
* avoid flushing rewrite rules in wp_install_maybe_enable_pretty_permalinks().
* Afterwards, on the 'wp_install' action, set the original (empty) permalink structure.
*/
if ( ! $permalink_structure ) {
add_action(
'wp_install',
static function () use ( $permalink_structure ) {
update_option( 'permalink_structure', $permalink_structure );
}
);
$permalink_structure = '/%postname%/';
}
add_option( 'permalink_structure', $permalink_structure );
}
add_option( 'permalink_structure', $permalink_structure );
}
);
);

$this->install_wordpress( $site_url, $active_theme, $active_plugins );
}
$this->install_wordpress( $site_url, $active_theme, $active_plugins );
}

// Restore the old prefix.
$prefix_cleanup();
// Restore the old prefix.
$prefix_cleanup();

// Return early if the plugin check object cache already exists.
if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) {
return;
}
// Return early if the plugin check object cache already exists.
if ( defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) && WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) {
return;
}

// Create the object-cache.php file.
if ( $wp_filesystem || WP_Filesystem() ) {
// Do not replace the object-cache.php file if it already exists.
if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
$wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' );
// Create the object-cache.php file.
if ( $wp_filesystem || WP_Filesystem() ) {
// Do not replace the object-cache.php file if it already exists.
if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
$wp_filesystem->copy( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php', WP_CONTENT_DIR . '/object-cache.php' );
}
}
} finally {
/**
* Fires after the runtime environment is set up, including when it exits early.
*
* @since 2.0.0
*
* @param array $context {
* Context for the hook.
*
* @type bool $early_exit Whether the method exited before completing all setup steps.
* }
*/
do_action( 'wp_plugin_check_after_runtime_setup', array( 'early_exit' => false ) );
}
}

Expand All @@ -105,38 +135,66 @@ static function () use ( $permalink_structure ) {
public function clean_up() {
global $wpdb, $wp_filesystem;

require_once ABSPATH . '/wp-admin/includes/upgrade.php';
/**
* Fires before the runtime environment is cleaned up.
*
* @since 2.0.0
*
* @param array $context {
* Context for the hook.
*
* @type bool $early_exit Whether the method exited before completing all cleanup steps.
* }
*/
do_action( 'wp_plugin_check_before_runtime_cleanup', array( 'early_exit' => false ) );

$prefix_cleanup = $this->amend_db_base_prefix();
$tables = $wpdb->tables();
try {
require_once ABSPATH . '/wp-admin/includes/upgrade.php';

$tables = $this->ignore_custom_tables( $tables );
$prefix_cleanup = $this->amend_db_base_prefix();
$tables = $wpdb->tables();

foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `$table`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
$tables = $this->ignore_custom_tables( $tables );

// Restore the old prefix.
$prefix_cleanup();
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `$table`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}

// Return early if the plugin check object cache does not exist.
if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) {
return;
}
// Restore the old prefix.
$prefix_cleanup();

// Remove the object-cache.php file.
if ( $wp_filesystem || WP_Filesystem() ) {
if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
// Return early if the plugin check object cache does not exist.
if ( ! defined( 'WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION' ) || ! WP_PLUGIN_CHECK_OBJECT_CACHE_DROPIN_VERSION ) {
return;
}

// Check the drop-in file matches the copy.
$original_content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/object-cache.php' );
$copy_content = $wp_filesystem->get_contents( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php' );
// Remove the object-cache.php file.
if ( $wp_filesystem || WP_Filesystem() ) {
if ( ! $wp_filesystem->exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
return;
}

// Check the drop-in file matches the copy.
$original_content = $wp_filesystem->get_contents( WP_CONTENT_DIR . '/object-cache.php' );
$copy_content = $wp_filesystem->get_contents( WP_PLUGIN_CHECK_PLUGIN_DIR_PATH . 'drop-ins/object-cache.copy.php' );

if ( $original_content && $original_content === $copy_content ) {
$wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' );
if ( $original_content && $original_content === $copy_content ) {
$wp_filesystem->delete( WP_CONTENT_DIR . '/object-cache.php' );
}
}
} finally {
/**
* Fires after the runtime environment is cleaned up, including when it exits early.
*
* @since 2.0.0
*
* @param array $context {
* Context for the hook.
*
* @type bool $early_exit Whether the method exited before completing all cleanup steps.
* }
*/
do_action( 'wp_plugin_check_after_runtime_cleanup', array( 'early_exit' => false ) );
}
}

Expand Down
Loading
Loading