PHP 8.x migration guide for WordPress — covers PHP 8.0 through 8.3 features, breaking changes, backward compatibility patterns, dynamic properties fixes, and step-by-step migration strategy for themes, plugins, and custom code.
Complete reference for migrating WordPress themes, plugins, and custom code from PHP 7.4 to PHP 8.0, 8.1, 8.2, and 8.3. Covers new features, breaking changes, backward compatibility, and the most common migration patterns.
Use with caution in WordPress hook callbacks. WordPress core functions do not guarantee parameter name stability across versions.
// BEFORE (PHP 7.4)
wp_insert_post( array(
'post_title' => 'My Post',
'post_content' => 'Content here',
'post_status' => 'publish',
'post_type' => 'page',
) );
// AFTER (PHP 8.0) — named arguments in your OWN functions only
function register_custom_block( string $name, string $title, string $icon = 'smiley', string $category = 'widgets' ): void {
// ...
}
register_custom_block( name: 'my-block', title: 'My Block', category: 'layout' );
// WARNING: Do NOT use named arguments with WordPress core functions or hooks.
// Parameter names may change between WP versions and break your code.
// BEFORE (PHP 7.4)
/** @param string|array $classes */
function filter_body_class( $classes ) { ... }
// AFTER (PHP 8.0)
function filter_body_class( string|array $classes ): string|array {
if ( is_array( $classes ) ) {
$classes[] = 'custom-class';
}
return $classes;
}
// BEFORE (PHP 7.4)
$user = wp_get_current_user();
$name = null;
if ( $user !== null ) {
$meta = get_user_meta( $user->ID, 'display_name', true );
if ( $meta !== null ) {
$name = $meta;
}
}
// AFTER (PHP 8.0)
$name = wp_get_current_user()?->display_name;
// BEFORE (PHP 7.4)
switch ( $post->post_status ) {
case 'publish':
$label = 'Published';
break;
case 'draft':
$label = 'Draft';
break;
case 'pending':
$label = 'Pending Review';
break;
default:
$label = 'Unknown';
break;
}
// AFTER (PHP 8.0)
$label = match ( $post->post_status ) {
'publish' => 'Published',
'draft' => 'Draft',
'pending' => 'Pending Review',
default => 'Unknown',
};
// BEFORE (PHP 7.4)
class My_Plugin {
private string $plugin_file;
private string $version;
private bool $debug;
public function __construct( string $plugin_file, string $version, bool $debug = false ) {
$this->plugin_file = $plugin_file;
$this->version = $version;
$this->debug = $debug;
}
}
// AFTER (PHP 8.0)
class My_Plugin {
public function __construct(
private string $plugin_file,
private string $version,
private bool $debug = false,
) {}
}
// BEFORE (PHP 7.4)
if ( strpos( $template, 'single-' ) === 0 ) { ... }
if ( strpos( $content, 'shortcode' ) !== false ) { ... }
if ( substr( $file, -4 ) === '.php' ) { ... }
// AFTER (PHP 8.0)
if ( str_starts_with( $template, 'single-' ) ) { ... }
if ( str_contains( $content, 'shortcode' ) ) { ... }
if ( str_ends_with( $file, '.php' ) ) { ... }
Note: WordPress 5.9+ includes polyfills for these functions. For older WP versions, use wp_polyfill or provide your own.
// BEFORE (PHP 7.4) — string constants scattered everywhere
define( 'POST_STATUS_PUBLISH', 'publish' );
define( 'POST_STATUS_DRAFT', 'draft' );
// AFTER (PHP 8.1)
enum PostStatus: string {
case Publish = 'publish';
case Draft = 'draft';
case Pending = 'pending';
case Private = 'private';
case Trash = 'trash';
public function label(): string {
return match ( $this ) {
self::Publish => __( 'Published', 'my-plugin' ),
self::Draft => __( 'Draft', 'my-plugin' ),
self::Pending => __( 'Pending Review', 'my-plugin' ),
self::Private => __( 'Private', 'my-plugin' ),
self::Trash => __( 'Trashed', 'my-plugin' ),
};
}
}
// Usage with WP_Query
$query = new WP_Query( [ 'post_status' => PostStatus::Publish->value ] );
// BEFORE (PHP 7.4)
class Plugin_Config {
private string $slug;
public function __construct( string $slug ) {
$this->slug = $slug;
}
public function get_slug(): string { return $this->slug; }
}
// AFTER (PHP 8.1)
class Plugin_Config {
public function __construct(
public readonly string $slug,
public readonly string $version,
public readonly string $file,
) {}
}
// $config->slug is publicly readable but cannot be modified after construction.
// BEFORE (PHP 7.4)
add_action( 'init', [ $this, 'register_post_types' ] );
add_filter( 'the_content', [ $this, 'filter_content' ] );
// AFTER (PHP 8.1)
add_action( 'init', $this->register_post_types( ... ) );
add_filter( 'the_content', $this->filter_content( ... ) );
// WARNING: Closure::fromCallable() or the ( ... ) syntax creates a Closure.
// remove_action / remove_filter will NOT work because object identity differs.
// Use first-class callables only when you never need to unhook.
// PHP 8.1 — require multiple interfaces
function process_entity( Countable&Iterator $items ): void {
foreach ( $items as $item ) {
// guaranteed to be both Countable and Iterator
}
}
// PHP 8.2
readonly class Meta_Box_Args {
public function __construct(
public string $id,
public string $title,
public string $screen,
public string $context = 'advanced',
public string $priority = 'default',
) {}
}
This is the single largest PHP 8.2 issue for WordPress. See Section 9 for full remediation.
// PHP 8.2 DEPRECATION — dynamic properties trigger E_DEPRECATED
$obj = new stdClass(); // stdClass is exempt
$obj->foo = 'bar'; // fine on stdClass
class My_Widget extends WP_Widget {
// This triggers deprecation in PHP 8.2:
// $this->custom_prop = 'value';
}
// PHP 8.2
function wp_cache_get_or_set( string $key ): string|false {
$cached = wp_cache_get( $key );
if ( $cached === false ) {
return false; // explicitly typed as false
}
return $cached;
}
// BEFORE (PHP 8.2)
class My_REST_Controller extends WP_REST_Controller {
const NAMESPACE = 'myplugin/v1'; // untyped
}
// AFTER (PHP 8.3)
class My_REST_Controller extends WP_REST_Controller {
const string NAMESPACE = 'myplugin/v1';
const int VERSION = 1;
}
// BEFORE (PHP 8.2)
function is_valid_json( string $data ): bool {
json_decode( $data );
return json_last_error() === JSON_ERROR_NONE;
}
// AFTER (PHP 8.3) — no decoding overhead
if ( json_validate( $raw_meta ) ) {
$meta = json_decode( $raw_meta, true );
}
// PHP 8.3
class Theme_Walker extends Walker_Nav_Menu {
#[\Override]
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ): void {
// If parent signature changes, PHP will throw a compile-time error.
}
}
// PHP 7.4: silently coerces — strlen( [] ) returns null with a warning
// PHP 8.0: throws TypeError for internal function type mismatches
// FIX: Always validate types before passing to internal functions
$length = is_string( $value ) ? strlen( $value ) : 0;
// PHP 7.4: passing null to non-nullable internal parameter gives a warning
// PHP 8.0+: still a warning; PHP 8.1: deprecation notice; PHP 9.0: TypeError
// Common WP offender:
trim( null ); // Deprecated in 8.1
htmlspecialchars( null ); // Deprecated in 8.1
// FIX:
trim( $value ?? '' );
htmlspecialchars( $value ?? '' );
// PHP 8.2 DEPRECATED: ${var} inside strings
$msg = "Hello ${name}"; // deprecated
$msg = "Hello {$name}"; // correct — use this form
$msg = "Hello $name"; // also fine for simple variables
// PHP 8.1 DEPRECATED: implicit narrowing float-to-int
$index = 3.0;
$arr[$index]; // deprecated — use (int) $index explicitly
| WordPress Version | Minimum PHP | Recommended PHP |
|---|---|---|
| WP 5.9 - 6.2 | 5.6 | 7.4+ |
| WP 6.3 - 6.4 | 7.0 | 8.0+ |
| WP 6.5+ | 7.2+ | 8.1+ |
Always check the latest readme.html in WP core for the current minimum.
/**
* Plugin Name: My Plugin
* Requires PHP: 8.0
* Requires at least: 6.3
*/
WordPress will prevent activation on incompatible PHP versions when Requires PHP is set.
WordPress Site Health (Tools > Site Health) checks PHP version and reports recommendations. Programmatic check:
$compat = wp_check_php_version();
if ( $compat && isset( $compat['is_acceptable'] ) && ! $compat['is_acceptable'] ) {
add_action( 'admin_notices', 'show_php_upgrade_notice' );
}
Phase 1: Audit
# Install PHPCompatibility coding standard
composer require --dev phpcompatibility/phpcompatibility-wp:"*"
# Configure phpcs
cat > phpcs.xml <<'XML'
<?xml version="1.0"?>
<ruleset name="PHP8Migration">
<rule ref="PHPCompatibilityWP"/>
<config name="testVersion" value="8.0-"/>
<file>./wp-content/themes/oshin_child/</file>
<file>./wp-content/plugins/my-plugin/</file>
</ruleset>
XML
# Run the scan
vendor/bin/phpcs --standard=phpcs.xml --report=full
Phase 2: Fix Deprecations
Address issues in order of severity:
Phase 3: CI Matrix Testing
# GitHub Actions example