Developer API

For plugin developers: start CompatNav scans in the background, read the report and its findings, and react when a scan finishes. CompatNav 1.1.0 and newer.

CompatNav 1.1.0 adds a small PHP API for add-ons: plain functions and actions that other plugins call on the same server. There are no HTTP endpoints, and nothing leaves the site. This page is for developers; site owners don’t need it.

Quick start

A small add-on that runs a full scan in WP-Cron and keeps the verdict. Schedule the my_addon_scan event the way you like (for example with wp_schedule_event(), weekly):

my-addon.php

<?php
// Scans the site in WP-Cron and keeps the verdict. Needs CompatNav 1.1.0 or newer.
add_action( 'my_addon_scan', function () {
	if ( ! function_exists( 'compatnav_start_scan' ) ) {
		return; // CompatNav is missing or older than 1.1.0.
	}
	$state = compatnav_get_scan_state();
	if ( ! $state['running'] ) {
		$state = compatnav_start_scan( array( 'initiator' => 'scheduled' ) );
	}
	$stop = microtime( true ) + 15; // A step takes up to 8 s: stay inside a 30 s limit.
	while ( ! is_wp_error( $state ) && $state['running'] && microtime( true ) < $stop ) {
		$state = compatnav_run_scan_step(); // One batch of files per call.
	}
	if ( ! is_wp_error( $state ) && $state['running'] ) {
		wp_schedule_single_event( time(), 'my_addon_scan' ); // Go on in the next WP-Cron run.
	}
} );
add_action( 'compatnav_scan_completed', function ( $scan_id, $summary ) {
	update_option( 'my_addon_verdict', $summary['verdict']['text'], false ); // Store only.
}, 10, 2 );

Output · WordPress 7.1.2, CompatNav 1.1.0, PHP 8.2.29 (WP-CLI)

$ wp cron event run my_addon_scan
Executed the cron event 'my_addon_scan' in 16.561s.
Success: Executed a total of 1 cron event.
$ wp cron event run my_addon_scan
Executed the cron event 'my_addon_scan' in 15.095s.
Success: Executed a total of 1 cron event.
$ wp cron event run my_addon_scan
Executed the cron event 'my_addon_scan' in 16.609s.
Success: Executed a total of 1 cron event.
$ wp cron event run my_addon_scan
Executed the cron event 'my_addon_scan' in 15.323s.
Success: Executed a total of 1 cron event.
$ wp cron event run my_addon_scan
Executed the cron event 'my_addon_scan' in 13.564s.
Success: Executed a total of 1 cron event.
$ wp option get my_addon_verdict
Safe to upgrade to PHP 8.5: nothing active will break

This is the real output: the example was loaded on our test site and its event was run with WP-CLI until it stopped rescheduling itself. Each run starts steps for up to 15 seconds, continues in the next WP-Cron run, and the listener stores the verdict when the scan completes. In a normal WP-Cron run the same code runs inside the cron request; WP-CLI only makes the runs visible.

Ground rules

  • Server-side only. Functions and actions in PHP; no HTTP endpoints, no external requests.
  • No scan work while a page is rendered. compatnav_start_scan() (it reads the headers of the installed plugins and themes) and compatnav_run_scan_step() (it lists and parses files) work only in WP-Cron, WP-CLI and AJAX requests. Anywhere else they return the error wrong_context. Every other function only reads stored data.
  • One request at a time. Functions that change the scan take the same lock as CompatNav’s own screen. While another request holds it, they return busy and change nothing: try again later. The lock expires after 60 seconds if a request dies. Reading functions never take it.
  • One scan at a time, one report. A new scan replaces the report only when it has finished. Cancelling it leaves the last report untouched.
  • Crash-safe. A step works within a budget: up to 200 files, 8 seconds (or a third of PHP’s time limit when that is shorter) and 70% of the memory limit. If PHP dies while reading a file, that file is recorded as not checked and the next step continues after it. If an exception is thrown during a step, you get unexpected_error; the file is recorded the same way, and the next call, even in the same request, continues.
  • No permission checks inside. WP-Cron and WP-CLI have no logged-in user. If you call the API for a visitor (an admin page, an AJAX handler), check compatnav_current_user_can_manage() and a nonce yourself, as CompatNav does.
  • No server paths, no code. Files are given as the report shows them (inside wp-content), and findings carry a fingerprint (a hash), not the code line.

Checking for the API

if ( function_exists( 'compatnav_api_version' ) && version_compare( compatnav_api_version(), '1.0', '>=' ) ) {
	// The API is there.
}

The action compatnav_loaded fires once per request, on plugins_loaded, with the API version.

Which PHP version a scan compares against

A scan reports what changes between the PHP version the website runs and the target version. From the command line that isn’t always the PHP running the code: WP-CLI and real server cron jobs can use a different PHP binary than the website. So CompatNav remembers the version it sees when its admin screen loads or its screen scans, with the date it was last seen (at most one write a day).

compatnav_start_scan() chooses, in this order:

  1. The current_version argument, if you pass one (source argument).
  2. In a web request (WP-Cron over HTTP, AJAX): the PHP running the request (source running_php).
  3. On the command line: the remembered website version (source stored), returned with its date so you can decide whether it is recent enough.
  4. On the command line with nothing remembered and no argument: the scan does not start, error site_php_version_unknown. Open Tools → CompatNav once in wp-admin, or pass current_version.

Functions

compatnav_api_version(): string

The API version, '1.0' for CompatNav 1.1.0.

compatnav_current_user_can_manage(): bool

Whether the current user may use CompatNav, with the screen’s rule: manage_options, or on multisite only super admins.

compatnav_get_site_php_version(): ?array

The remembered website PHP version, or null if it was never seen: [ 'version' => '8.5', 'full' => '8.5.11', 'seen_at' => 1790000000 ] (Unix time).

compatnav_start_scan( array $args = [] ): array|WP_Error

Creates a full scan of all installed plugins and themes, like the screen’s “Start new scan”. It reads the plugins’ and themes’ headers and saves the scan; the first step lists and parses the files.

ArgumentTypeDefaultNotes
targetstringthe screen’s saved target8.0 … 8.5. Doesn’t change the saved setting.
skip_bundledboolthe screen’s saved settingSkip bundled libraries. Doesn’t change the saved setting.
current_versionstringsee above7.4, 8.0 … 8.5.
initiatorstringapischeduled, wp-cli or api. Shown on the report as “Started by: …”.

Unknown keys and wrong types are refused; a key set to null counts as not given. Returns the new scan’s state (below). Errors: invalid_argument, invalid_target, invalid_version, invalid_initiator, wrong_context (all before the lock), busy, scan_running, site_php_version_unknown, unexpected_error. Fires compatnav_scan_started.

compatnav_run_scan_step(): array|WP_Error

Does one batch of scan work and returns the scan state. Call it again while running is true and your request has time left. With nothing to do (no scan, or the scan is paused or finished), it returns the state without an error. Errors: invalid_argument (it takes no arguments yet), wrong_context, scan_running, busy, unexpected_error. Fires compatnav_scan_completed when this step finishes the scan.

compatnav_get_scan_state(): array

The current or last scan, always with the same keys (with no scan yet, status is idle and the other keys hold empty values). The main ones:

KeyMeaning
statusidle, collecting, parsing, paused, completed or cancelled
runningcollecting or parsing
stalledrunning, but no step for more than stalled_after_seconds (60)
last_step_at, seconds_since_last_stepwhen a step (or start, pause, resume, cancel) last updated it
scan_id, initiatorthe scan and who started it (screen, scheduled, wp-cli, api)
scopetype: full, or partial (a re-scan of one plugin or theme from the screen), and sources
phase, percent, processed, countersprogress; counters: files, parsed, not_parsable, crashed, skipped, not_checked
current_version, target_version, current_version_source, site_php, skip_bundledthe versions compared and where the current one came from
contexthow the scan runs: initiator, sapi, memory_limit, max_execution_time, limits_varied, current_version_source, site_php (details below)
started_at, finished_at, active_secondstimes
last_reportscan_id, finished_at and versions of the last completed report, or null

compatnav_cancel_scan(): array|WP_Error

Cancels a running or paused scan and deletes its rows; the last report stays. With nothing to cancel it returns the state. Errors: scan_running (from a listener), busy, unexpected_error. Fires compatnav_scan_cancelled.

compatnav_get_report_summary(): ?array

The last completed report, with the same verdict and counts as the screen, or null. Ignored findings are left out of all counts, and “active” is judged now, as on the screen.

KeyMeaning
verdictcode: safe, not_ready or not_fully_checked; text: the sentence the screen shows. safe only when nothing active will break and every active file was checked.
active, inactivecounts of places that will break (errors) and deprecation notices (warnings)
filestotal, checked, not_checked, not_checked_active
sourcesone entry per plugin or theme with findings or unchecked files: id, type, slug, name, version_scanned, version_now, installed, active, status (update_available, no_update, not_wporg, custom, unknown, removed), new_version, counts
coveragethe files that could not be checked, with a reason code (at most 100, as on the screen), and the files skipped on purpose
scope, full_scan, rescannedwhat changed the report last, the full scan it is based on, and plugins re-scanned since
ignored, updates_checked_at, context, versions, times, legacyas named

compatnav_get_findings( array $args = [] ): array|WP_Error

The findings of the last report, in chunks, never all at once.

ArgumentDefaultNotes
afternonethe previous chunk’s next_after (a string like "57.4.12345"); leave it out for the first chunk
limit1001–500
sourceallone plugin or theme ID from the summary, e.g. plugin:akismet/akismet.php
severityallerror or warning
activeallall, active or inactive
ignoredexcludeexclude (like the report), include or only

It returns scan_id, items and next_after (null after the last chunk). Each item has id, source_id, source_type, source_active, file, line, rule_id, php_version, severity, confidence, message, more_count (further places of the same check in the file), bundled, fingerprint and ignored. The fingerprint identifies a finding across scans, even when its line number changes.

Paging never mixes two reports. The cursor carries the report and its revision. If a scan finishes between two chunks (or a re-scan is merged into the report), the next call returns report_changed: start again from the first chunk. Within one report, you get every finding once, in ID order, without gaps.

Counting the places per check in the active plugins and themes of our test site.

findings-by-rule.php

<?php
// Counts the findings of the active plugins and themes per check, in chunks of 500.
$counts = array();
$after  = null; // No cursor for the first chunk.
do {
	$chunk = compatnav_get_findings( array( 'active' => 'active', 'limit' => 500, 'after' => $after ) );
	if ( is_wp_error( $chunk ) ) {
		echo $chunk->get_error_code(), "\n"; // 'report_changed': a scan finished; start again later.
		return;
	}
	foreach ( $chunk['items'] as $finding ) {
		$counts[ $finding['rule_id'] ] = ( $counts[ $finding['rule_id'] ] ?? 0 ) + 1 + $finding['more_count'];
	}
	$after = $chunk['next_after'];
} while ( null !== $after );
arsort( $counts );
foreach ( $counts as $rule_id => $places ) {
	echo str_pad( $rule_id, 36 ), $places, "\n";
}

Output · WordPress 7.1.2, CompatNav 1.1.0, PHP 8.2.29 (WP-CLI)

php84.implicitly_nullable           9
php84.trigger_error_user_error      4
php84.csv_escape_default            4
php82.relative_callable             3
php80.libxml_disable_entity_loader  2
php85.reflection_set_accessible     2
php84.deprecated_function           1
php85.deprecated_function           1
php82.dynamic_property              1
php82.utf8_encode_decode            1

compatnav_get_rules(): array

Every check, keyed by its rule ID: php_version, severity, explanation (the plain-language text the report shows), fix_before and fix_after. It includes syntax_incompatible, the finding for code the target version can’t read at all, so every rule_id a finding can carry is listed (CompatNav 1.1.0: 58 checks plus syntax_incompatible).

Errors

CodeMeaning
invalid_argumentan unknown key, a wrong type, a malformed cursor, a limit out of range, arguments to the step
invalid_targetnot one of 8.0 … 8.5
invalid_versioncurrent_version not one of 7.4, 8.0 … 8.5
invalid_initiatornot scheduled, wp-cli or api
wrong_contextstart or step outside WP-Cron, WP-CLI and AJAX
busyanother request holds the scan lock; try again later
scan_runninga scan is already running or paused, or the call comes from a listener of a scan action
site_php_version_unknowncommand line, no remembered website version, no current_version
unexpected_erroran exception during the operation; the file being read is recorded as not checked, and the next call continues
report_changedthe report changed since your cursor; start again from the first chunk

The codes are stable. The messages are for people, may change, and come in the site’s language.

Actions

ActionArgumentsWhen
compatnav_loadedstring $api_versiononce per request, on plugins_loaded
compatnav_scan_startedint $scan_id, array $contextevery scan start, including the screen’s
compatnav_scan_completedint $scan_id, array $summary, array $contextwhen a scan has finished and its report is stored; $summary is the report summary at that moment
compatnav_scan_cancelledint $scan_id, array $contextafter a cancel

$context has scope, initiator, target_version, current_version, current_version_source, site_php, skip_bundled and context.

Rules for listeners. The actions run inside the scan request, after its work is saved, while the scan lock is still held. So a listener must be fast, must not start, step or cancel scans (those calls return scan_running), and must not send emails or call remote services. Store what you need and do the rest in a separate request, for example wp_schedule_single_event( time(), 'my_addon_after_scan', array( $scan_id ) ).

A listener that throws can’t stop the scan: CompatNav catches it, writes one line per action and request to the PHP error log, in the form CompatNav: a listener of <action> failed and was skipped: <exception class>: <message>, and the scan continues. The exception ends WordPress’s loop over that action, so later listeners of the same action don’t run in that request: catch your own errors.

Comparing scans

  • Compare full scans with full scans. A re-scan of one plugin or theme (scope.type = partial) replaces that plugin’s rows in the report; it is not a new baseline.
  • Coverage is not a problem. Which heavy files can be checked depends on how a scan ran. On our test site, WP-CLI ran without a memory limit and checked a very dense file that scans with a 128 MB or 256 MB limit skipped. The context (SAPI, memory and time limits, the version source) says how each scan ran, so a file that moved between “could not be checked” and “checked” can be told apart from a new problem.

Stability

  • Within API 1.x, changes are additive: new functions, new optional arguments, new keys in returned arrays, new actions. Existing ones keep their meaning. Ignore keys you don’t know.
  • Something that has to go is marked deprecated first and kept for at least two minor releases of CompatNav; it is removed only in API 2.0.
  • Rule IDs are part of the contract: an existing ID keeps its meaning, and a withdrawn one is never reused. The same goes for error codes, verdict codes, statuses and scope types.
  • Texts come in the site’s language (Settings → General), not the current user’s, and may be reworded between releases: compare codes and IDs, never texts.

Changelog

  • 1.0 (CompatNav 1.1.0): first version.