Skip to content

Instantly share code, notes, and snippets.

@pierrerocket
Last active March 11, 2025 15:43
Show Gist options
  • Save pierrerocket/860a8fb85acffe34e6acf21cbca23ebd to your computer and use it in GitHub Desktop.
Save pierrerocket/860a8fb85acffe34e6acf21cbca23ebd to your computer and use it in GitHub Desktop.
WP CLI Command to Find and clean orphaned ACF Postmeta from all Posts. Includes logging.
<?php
/**
* Find orphaned ACF field data in post meta.
*
* Searches for ACF field data in all posts where the field definitions
* no longer exist in any field groups.
*
* ## OPTIONS
*
* [--dry-run]
* : Run without making any changes (just report findings).
*
* [--log=<filename>]
* : Save output to log file. If no filename is given, uses 'acf-orphaned-fields-{timestamp}.log'.
*
* ## EXAMPLES
*
* wp acf orphaned-fields
* wp acf orphaned-fields --dry-run
* wp acf orphaned-fields --log
* wp acf orphaned-fields --dry-run --log
* wp acf orphaned-fields --log=my-acf-report.log
*/
// Add this to a custom plugin or your theme's functions.php
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'acf orphaned-fields', 'find_orphaned_acf_fields' );
}
function find_orphaned_acf_fields( $args, $assoc_args ) {
$dry_run = isset( $assoc_args['dry-run'] );
$log_enabled = isset( $assoc_args['log'] );
$log_file = null;
$log_contents = '';
// Set up logging if enabled
if ($log_enabled) {
$log_filename = $assoc_args['log'];
// If no filename provided, create one with timestamp
if ($log_filename === true) {
$timestamp = date('Y-m-d-His');
$log_filename = "acf-orphaned-fields-{$timestamp}.log";
}
// Make sure log directory exists
$upload_dir = wp_upload_dir();
$log_dir = trailingslashit($upload_dir['basedir']) . 'acf-logs';
if (!file_exists($log_dir)) {
// Try to create directory with proper error handling
if (!wp_mkdir_p($log_dir)) {
WP_CLI::warning("Could not create log directory at: {$log_dir}");
WP_CLI::log("Saving log to uploads base directory instead.");
$log_dir = $upload_dir['basedir'];
}
}
// Make sure log directory is writable
if (!is_writable($log_dir)) {
WP_CLI::warning("Log directory is not writable: {$log_dir}");
WP_CLI::log("Saving log to system temp directory instead.");
$log_dir = sys_get_temp_dir();
}
$log_file = trailingslashit($log_dir) . $log_filename;
// Log header
$log_contents .= "==== ACF ORPHANED FIELDS REPORT ====\n";
$log_contents .= "Generated: " . date('Y-m-d H:i:s') . "\n";
$log_contents .= "WordPress Site: " . get_bloginfo('name') . " (" . get_bloginfo('url') . ")\n\n";
WP_CLI::log("Logging enabled. Output will be saved to: {$log_file}");
}
// Custom log function that outputs to both CLI and log file if enabled
$logger = function($message) use (&$log_contents, $log_enabled) {
WP_CLI::log($message);
if ($log_enabled) {
$log_contents .= $message . "\n";
}
};
// Make sure ACF is active
if ( !function_exists('acf_get_field_groups') ) {
WP_CLI::error( 'Advanced Custom Fields is not active.' );
return;
}
// Get all ACF field groups
$field_groups = acf_get_field_groups();
if ( empty( $field_groups ) ) {
WP_CLI::error( 'No ACF field groups found.' );
return;
}
// Get all existing field keys
$existing_field_keys = array();
foreach ( $field_groups as $field_group ) {
$fields = acf_get_fields( $field_group );
if ( !empty( $fields ) ) {
$existing_field_keys = array_merge( $existing_field_keys, get_all_acf_field_keys( $fields ) );
}
}
WP_CLI::log( sprintf( 'Found %d valid ACF field keys across all field groups.', count( $existing_field_keys ) ) );
// Get all published posts of any type
$all_post_types = get_post_types(['public' => true], 'names');
$args = array(
'post_type' => $all_post_types,
'posts_per_page' => -1,
'post_status' => 'any',
);
$posts = get_posts($args);
WP_CLI::log( sprintf( 'Examining %d posts, pages, and custom post types...', count( $posts ) ) );
$orphaned_count = 0;
$posts_with_orphans = 0;
// Progress bar for CLI
$progress = \WP_CLI\Utils\make_progress_bar( 'Scanning posts', count( $posts ) );
// Loop through each post
foreach ( $posts as $post ) {
$post_id = $post->ID;
$post_has_orphans = false;
// Get all post meta for this post
$post_meta = get_post_meta($post_id);
$acf_fields = [];
$orphaned_fields = [];
// Identify ACF fields in post meta
foreach ( $post_meta as $meta_key => $meta_values ) {
// Skip non-ACF fields
if ( !is_acf_field_key($meta_key) && !is_acf_field_ref($meta_key) ) {
continue;
}
$meta_value = $meta_values[0]; // get_post_meta returns array of values
// The issue is that regular ACF field values don't have "field_" in their meta_key
// They have normal names like "color" or "product_description"
// Only the reference fields (starting with _) contain the actual field key
// If this is a reference field (starts with _)
if ( is_acf_field_ref($meta_key) ) {
$field_key = $meta_value; // The value of _fieldname contains the actual field key
$field_name = substr($meta_key, 1); // Remove leading underscore to get field name
// Only process valid field keys
if ( strpos($field_key, 'field_') !== 0 ) {
continue;
}
// Get the actual field value
$field_value = get_post_meta($post_id, $field_name, true);
// Check if this field is orphaned (no longer exists in any field group)
if ( !in_array($field_key, $existing_field_keys) ) {
// Add to orphaned fields
$orphaned_fields[$field_key] = [
'field_key' => $field_key,
'field_name' => $field_name,
'data_preview' => truncate_meta_value($field_value),
];
$orphaned_count++;
$post_has_orphans = true;
} else {
// Add to regular ACF fields
$acf_fields[$field_key] = [
'field_key' => $field_key,
'field_name' => $field_name,
'field_value' => truncate_meta_value($field_value),
];
}
}
}
// Output information if post has orphaned fields
if ( $post_has_orphans ) {
$posts_with_orphans++;
$logger( "\n----- Post ID: {$post_id} -----" );
$logger( "Title: " . $post->post_title );
$logger( "Type: " . $post->post_type );
$logger( "Edit URL: " . get_edit_post_link($post_id) );
// Use ACF's API to get the actual fields for this post
$acf_fields = [];
// Get field groups for this post
$field_groups_for_post = acf_get_field_groups(['post_id' => $post_id]);
if (!empty($field_groups_for_post)) {
$logger( "\nActive ACF Field Groups:" );
foreach ($field_groups_for_post as $group) {
$logger( "- {$group['title']}" );
// Get fields for this group
$fields = acf_get_fields($group);
if (!empty($fields)) {
foreach ($fields as $field) {
$acf_fields[$field['key']] = [
'field_key' => $field['key'],
'field_name' => $field['name'],
'field_label' => $field['label'],
'field_group' => $group['title']
];
}
}
}
$logger( "\nActive ACF Fields:" );
foreach ( $acf_fields as $field ) {
$value = get_field($field['field_name'], $post_id);
$preview = truncate_meta_value($value);
$logger( "- {$field['field_label']} ({$field['field_key']})" );
$logger( " Group: {$field['field_group']}" );
$logger( " Data: {$preview}" );
}
} else {
$logger( "\nNo active ACF field groups." );
}
if (!empty($orphaned_fields)) {
$logger( "\nOrphaned ACF Fields:" );
foreach ( $orphaned_fields as $field ) {
$logger( "- {$field['field_name']} ({$field['field_key']})" );
$logger( " Data: {$field['data_preview']}" );
// Try to find what field group this might have belonged to
// by looking at the field key pattern
$probable_group = "Unknown";
// Some field keys follow patterns that might help identify their group
// Loop through existing field groups to see if there's a pattern match
foreach ($field_groups as $group) {
$group_fields = acf_get_fields($group);
if (!empty($group_fields) && !empty($group_fields[0]['key'])) {
// Compare first 10 characters of the key which often identify the group
$sample_key = substr($group_fields[0]['key'], 0, 10);
$orphan_key_start = substr($field['field_key'], 0, 10);
if ($sample_key === $orphan_key_start) {
$probable_group = $group['title'];
break;
}
}
}
$logger( " Probable Field Group: {$probable_group}" );
}
}
}
$progress->tick();
}
$progress->finish();
if ( $orphaned_count === 0 ) {
$logger( 'No orphaned ACF field data found.' );
// Save log if enabled
if ($log_enabled) {
$log_contents .= "\nNo orphaned ACF field data found.";
file_put_contents($log_file, $log_contents);
WP_CLI::success("Log saved to: {$log_file}");
}
return;
}
$logger( sprintf( "\nFound %d orphaned ACF field entries across %d posts.",
$orphaned_count, $posts_with_orphans ) );
// Save log if enabled
if ($log_enabled) {
try {
// Make sure the directory exists one more time before writing
$log_dir = dirname($log_file);
if (!file_exists($log_dir)) {
mkdir($log_dir, 0755, true);
}
// Write the file with proper error handling
if (file_put_contents($log_file, $log_contents)) {
WP_CLI::success("Log saved to: {$log_file}");
} else {
// Try system temp directory as a last resort
$temp_log_file = sys_get_temp_dir() . '/' . basename($log_file);
if (file_put_contents($temp_log_file, $log_contents)) {
WP_CLI::success("Log saved to temporary location: {$temp_log_file}");
} else {
WP_CLI::error("Failed to save log file.");
}
}
} catch (Exception $e) {
WP_CLI::error("Exception while saving log: " . $e->getMessage());
}
}
// Offer to delete if not in dry run mode
if ( !$dry_run && $orphaned_count > 0 ) {
WP_CLI::confirm( "Would you like to delete all orphaned ACF field data found?" );
// Reset progress bar
$progress = \WP_CLI\Utils\make_progress_bar( 'Deleting orphaned fields', count( $posts ) );
$deleted = 0;
foreach ( $posts as $post ) {
$post_id = $post->ID;
$post_meta = get_post_meta($post_id);
foreach ( $post_meta as $meta_key => $meta_values ) {
// Skip non-ACF fields
if ( !is_acf_field_key($meta_key) && !is_acf_field_ref($meta_key) ) {
continue;
}
$meta_value = $meta_values[0];
// Determine if this is a field key or reference
$is_field_ref = is_acf_field_ref($meta_key);
$field_key = $is_field_ref ? $meta_value : $meta_key;
// Only process valid field keys
if ( strpos($field_key, 'field_') !== 0 ) {
continue;
}
// Delete if orphaned
if ( !in_array($field_key, $existing_field_keys) ) {
if ( delete_post_meta( $post_id, $meta_key ) ) {
$deleted++;
}
}
}
$progress->tick();
}
$progress->finish();
$success_message = sprintf( 'Deleted %d orphaned ACF field entries.', $deleted );
$logger( $success_message );
// Add deletion report to log if enabled
if ($log_enabled) {
$log_contents .= "\n\n==== DELETION REPORT ====\n";
$log_contents .= $success_message . "\n";
file_put_contents($log_file, $log_contents);
WP_CLI::success("Updated log saved to: {$log_file}");
}
}
}
/**
* Check if a meta key is an ACF field key
*/
function is_acf_field_key( $meta_key ) {
return strpos( $meta_key, 'field_' ) === 0;
}
/**
* Check if a meta key is an ACF field reference (starts with underscore)
*/
function is_acf_field_ref( $meta_key ) {
return strpos( $meta_key, '_' ) === 0 && strpos( $meta_key, '_field_' ) !== 0;
}
/**
* Recursively get all field keys from ACF fields
*/
function get_all_acf_field_keys( $fields, &$keys = array() ) {
foreach ( $fields as $field ) {
$keys[] = $field['key'];
// Handle sub-fields for repeaters, flexible content, etc.
if ( !empty( $field['sub_fields'] ) ) {
get_all_acf_field_keys( $field['sub_fields'], $keys );
}
// Handle layouts for flexible content fields
if ( !empty( $field['layouts'] ) ) {
foreach ( $field['layouts'] as $layout ) {
if ( !empty( $layout['sub_fields'] ) ) {
get_all_acf_field_keys( $layout['sub_fields'], $keys );
}
}
}
}
return $keys;
}
/**
* Truncate meta value for display
*/
function truncate_meta_value( $value ) {
if ( is_array( $value ) ) {
$value = json_encode( $value );
} elseif ( is_object( $value ) ) {
$value = json_encode( $value );
}
if ( is_string( $value ) ) {
if ( strlen( $value ) > 50 ) {
return substr( $value, 0, 47 ) . '...';
}
return $value;
}
return print_r( $value, true );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment