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 );
$ 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 breakThis 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) andcompatnav_run_scan_step()(it lists and parses files) work only in WP-Cron, WP-CLI and AJAX requests. Anywhere else they return the errorwrong_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
busyand 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:
- The
current_versionargument, if you pass one (sourceargument). - In a web request (WP-Cron over HTTP, AJAX): the PHP running the request (source
running_php). - On the command line: the remembered website version (source
stored), returned with its date so you can decide whether it is recent enough. - 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 passcurrent_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.
| Argument | Type | Default | Notes |
|---|---|---|---|
target | string | the screen’s saved target | 8.0 … 8.5. Doesn’t change the saved setting. |
skip_bundled | bool | the screen’s saved setting | Skip bundled libraries. Doesn’t change the saved setting. |
current_version | string | see above | 7.4, 8.0 … 8.5. |
initiator | string | api | scheduled, 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:
| Key | Meaning |
|---|---|
status | idle, collecting, parsing, paused, completed or cancelled |
running | collecting or parsing |
stalled | running, but no step for more than stalled_after_seconds (60) |
last_step_at, seconds_since_last_step | when a step (or start, pause, resume, cancel) last updated it |
scan_id, initiator | the scan and who started it (screen, scheduled, wp-cli, api) |
scope | type: full, or partial (a re-scan of one plugin or theme from the screen), and sources |
phase, percent, processed, counters | progress; counters: files, parsed, not_parsable, crashed, skipped, not_checked |
current_version, target_version, current_version_source, site_php, skip_bundled | the versions compared and where the current one came from |
context | how the scan runs: initiator, sapi, memory_limit, max_execution_time, limits_varied, current_version_source, site_php (details below) |
started_at, finished_at, active_seconds | times |
last_report | scan_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.
| Key | Meaning |
|---|---|
verdict | code: 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, inactive | counts of places that will break (errors) and deprecation notices (warnings) |
files | total, checked, not_checked, not_checked_active |
sources | one 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 |
coverage | the 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, rescanned | what changed the report last, the full scan it is based on, and plugins re-scanned since |
ignored, updates_checked_at, context, versions, times, legacy | as named |
compatnav_get_findings( array $args = [] ): array|WP_Error
The findings of the last report, in chunks, never all at once.
| Argument | Default | Notes |
|---|---|---|
after | none | the previous chunk’s next_after (a string like "57.4.12345"); leave it out for the first chunk |
limit | 100 | 1–500 |
source | all | one plugin or theme ID from the summary, e.g. plugin:akismet/akismet.php |
severity | all | error or warning |
active | all | all, active or inactive |
ignored | exclude | exclude (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.
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";
}
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 1compatnav_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
| Code | Meaning |
|---|---|
invalid_argument | an unknown key, a wrong type, a malformed cursor, a limit out of range, arguments to the step |
invalid_target | not one of 8.0 … 8.5 |
invalid_version | current_version not one of 7.4, 8.0 … 8.5 |
invalid_initiator | not scheduled, wp-cli or api |
wrong_context | start or step outside WP-Cron, WP-CLI and AJAX |
busy | another request holds the scan lock; try again later |
scan_running | a scan is already running or paused, or the call comes from a listener of a scan action |
site_php_version_unknown | command line, no remembered website version, no current_version |
unexpected_error | an exception during the operation; the file being read is recorded as not checked, and the next call continues |
report_changed | the 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
| Action | Arguments | When |
|---|---|---|
compatnav_loaded | string $api_version | once per request, on plugins_loaded |
compatnav_scan_started | int $scan_id, array $context | every scan start, including the screen’s |
compatnav_scan_completed | int $scan_id, array $summary, array $context | when a scan has finished and its report is stored; $summary is the report summary at that moment |
compatnav_scan_cancelled | int $scan_id, array $context | after 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.