Last active
December 8, 2025 07:27
-
-
Save krasenslavov/888821219e43cdfc85c7288205a7102d to your computer and use it in GitHub Desktop.
ACF Cleaner: Remove Orphaned Custom Field Data in WordPress
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // ALWAYS run dry run first | |
| $scan_results = clean_orphaned_acf_meta( true ); | |
| // Review results | |
| error_log( 'Found ' . $scan_results['count'] . ' orphaned entries' ); | |
| // Only proceed after manual review | |
| if ( $scan_results['count'] > 0 && $scan_results['count'] < 1000 ) { | |
| // Reasonable number, safe to clean | |
| $clean_results = clean_orphaned_acf_meta( false ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Backup ACF meta before cleaning | |
| */ | |
| function backup_acf_meta_before_clean() { | |
| global $wpdb; | |
| $orphans = scan_orphaned_acf_meta(); | |
| if ( empty( $orphans ) ) { | |
| return false; | |
| } | |
| // Create backup table | |
| $backup_table = $wpdb->prefix . 'acf_meta_backup_' . time(); | |
| $wpdb->query( | |
| "CREATE TABLE {$backup_table} LIKE {$wpdb->postmeta}" | |
| ); | |
| // Insert orphaned meta into backup | |
| foreach ( $orphans as $orphan ) { | |
| $wpdb->insert( | |
| $backup_table, | |
| array( | |
| 'meta_id' => $orphan->meta_id, | |
| 'post_id' => $orphan->post_id, | |
| 'meta_key' => $orphan->meta_key, | |
| 'meta_value' => $orphan->meta_value | |
| ), | |
| array( '%d', '%d', '%s', '%s' ) | |
| ); | |
| } | |
| return $backup_table; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Remove orphaned ACF meta entries | |
| */ | |
| function clean_orphaned_acf_meta( $dry_run = true ) { | |
| global $wpdb; | |
| // Get orphaned entries | |
| $orphans = scan_orphaned_acf_meta(); | |
| if ( empty( $orphans ) ) { | |
| return array( | |
| 'status' => 'success', | |
| 'message' => 'No orphaned ACF meta found', | |
| 'count' => 0 | |
| ); | |
| } | |
| $deleted_count = 0; | |
| foreach ( $orphans as $orphan ) { | |
| if ( ! $dry_run ) { | |
| // Actually delete the meta entry | |
| $deleted = $wpdb->delete( | |
| $wpdb->postmeta, | |
| array( 'meta_id' => $orphan->meta_id ), | |
| array( '%d' ) | |
| ); | |
| if ( $deleted ) { | |
| $deleted_count++; | |
| } | |
| } | |
| } | |
| return array( | |
| 'status' => 'success', | |
| 'message' => $dry_run ? 'Dry run completed' : 'Orphaned meta cleaned', | |
| 'count' => $dry_run ? count( $orphans ) : $deleted_count, | |
| 'orphans' => $orphans | |
| ); | |
| } | |
| /** | |
| * Admin page for ACF cleaner | |
| */ | |
| function acf_cleaner_admin_page() { | |
| // Check user capabilities | |
| if ( ! current_user_can( 'manage_options' ) ) { | |
| return; | |
| } | |
| // Handle form submission | |
| if ( isset( $_POST['acf_clean_action'] ) && check_admin_referer( 'acf_cleaner_nonce' ) ) { | |
| $dry_run = $_POST['acf_clean_action'] === 'scan'; | |
| $results = clean_orphaned_acf_meta( $dry_run ); | |
| ?> | |
| <div class="notice notice-<?php echo esc_attr( $results['status'] === 'success' ? 'success' : 'error' ); ?>"> | |
| <p><?php echo esc_html( $results['message'] ); ?></p> | |
| <p>Found/Cleaned: <?php echo intval( $results['count'] ); ?> orphaned entries</p> | |
| </div> | |
| <?php | |
| } | |
| ?> | |
| <div class="wrap"> | |
| <h1>ACF Cleaner</h1> | |
| <p>Scan and clean orphaned Advanced Custom Fields data from your database.</p> | |
| <form method="post" action=""> | |
| <?php wp_nonce_field( 'acf_cleaner_nonce' ); ?> | |
| <table class="form-table"> | |
| <tr> | |
| <th scope="row">Action</th> | |
| <td> | |
| <button type="submit" name="acf_clean_action" value="scan" class="button button-secondary"> | |
| Scan for Orphans (Dry Run) | |
| </button> | |
| <button type="submit" name="acf_clean_action" value="clean" class="button button-primary" | |
| onclick="return confirm('Are you sure? This will permanently delete orphaned meta entries.');"> | |
| Clean Orphaned Data | |
| </button> | |
| </td> | |
| </tr> | |
| </table> | |
| </form> | |
| </div> | |
| <?php | |
| } | |
| // Add admin menu | |
| add_action( 'admin_menu', function() { | |
| add_management_page( | |
| 'ACF Cleaner', | |
| 'ACF Cleaner', | |
| 'manage_options', | |
| 'acf-cleaner', | |
| 'acf_cleaner_admin_page' | |
| ); | |
| } ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Clean orphaned ACF meta for specific post type | |
| */ | |
| function clean_acf_meta_by_post_type( $post_type, $dry_run = true ) { | |
| global $wpdb; | |
| $orphans = scan_orphaned_acf_meta(); | |
| $cleaned = array(); | |
| foreach ( $orphans as $orphan ) { | |
| // Check if this orphan belongs to the specified post type | |
| $post = get_post( $orphan->post_id ); | |
| if ( $post && $post->post_type === $post_type ) { | |
| if ( ! $dry_run ) { | |
| $wpdb->delete( | |
| $wpdb->postmeta, | |
| array( 'meta_id' => $orphan->meta_id ), | |
| array( '%d' ) | |
| ); | |
| } | |
| $cleaned[] = $orphan; | |
| } | |
| } | |
| return $cleaned; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Scan database for orphaned ACF meta entries | |
| */ | |
| function scan_orphaned_acf_meta() { | |
| global $wpdb; | |
| // Get all ACF field keys from active field groups | |
| $field_groups = acf_get_field_groups(); | |
| $valid_keys = array(); | |
| foreach ( $field_groups as $group ) { | |
| $fields = acf_get_fields( $group['key'] ); | |
| if ( $fields ) { | |
| foreach ( $fields as $field ) { | |
| $valid_keys[] = $field['key']; | |
| $valid_keys[] = $field['name']; | |
| // Handle nested fields (repeaters, groups) | |
| if ( isset( $field['sub_fields'] ) ) { | |
| $valid_keys = array_merge( $valid_keys, get_sub_field_keys( $field['sub_fields'] ) ); | |
| } | |
| } | |
| } | |
| } | |
| // Search postmeta for ACF entries not in valid keys | |
| $orphaned_meta = $wpdb->get_results( | |
| $wpdb->prepare( | |
| "SELECT meta_id, post_id, meta_key, meta_value | |
| FROM {$wpdb->postmeta} | |
| WHERE meta_key LIKE %s | |
| AND meta_key NOT LIKE %s", | |
| 'field_%', | |
| '\_field_%' | |
| ) | |
| ); | |
| $orphans = array(); | |
| foreach ( $orphaned_meta as $meta ) { | |
| // Check if this meta_key matches any valid field | |
| $is_valid = false; | |
| foreach ( $valid_keys as $key ) { | |
| if ( $meta->meta_key === $key || $meta->meta_key === '_' . $key ) { | |
| $is_valid = true; | |
| break; | |
| } | |
| } | |
| if ( ! $is_valid ) { | |
| $orphans[] = $meta; | |
| } | |
| } | |
| return $orphans; | |
| } | |
| /** | |
| * Recursively get sub-field keys | |
| */ | |
| function get_sub_field_keys( $sub_fields ) { | |
| $keys = array(); | |
| foreach ( $sub_fields as $field ) { | |
| $keys[] = $field['key']; | |
| $keys[] = $field['name']; | |
| if ( isset( $field['sub_fields'] ) ) { | |
| $keys = array_merge( $keys, get_sub_field_keys( $field['sub_fields'] ) ); | |
| } | |
| } | |
| return $keys; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Schedule automatic ACF cleaning | |
| */ | |
| function schedule_acf_cleaning() { | |
| if ( ! wp_next_scheduled( 'acf_auto_clean' ) ) { | |
| wp_schedule_event( time(), 'weekly', 'acf_auto_clean' ); | |
| } | |
| } | |
| add_action( 'wp', 'schedule_acf_cleaning' ); | |
| /** | |
| * Execute scheduled cleaning | |
| */ | |
| function execute_acf_auto_clean() { | |
| // Only clean if orphan count exceeds threshold | |
| $orphans = scan_orphaned_acf_meta(); | |
| if ( count( $orphans ) > 100 ) { | |
| // Create backup first | |
| $backup_table = backup_acf_meta_before_clean(); | |
| // Clean orphans | |
| $results = clean_orphaned_acf_meta( false ); | |
| // Log results | |
| error_log( sprintf( | |
| 'ACF Auto Clean: Removed %d orphaned entries. Backup: %s', | |
| $results['count'], | |
| $backup_table | |
| ) ); | |
| } | |
| } | |
| add_action( 'acf_auto_clean', 'execute_acf_auto_clean' ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * WP-CLI command for ACF cleaning | |
| */ | |
| if ( defined( 'WP_CLI' ) && WP_CLI ) { | |
| class ACF_Cleaner_Command { | |
| /** | |
| * Scan for orphaned ACF meta | |
| * | |
| * ## EXAMPLES | |
| * | |
| * wp acf-cleaner scan | |
| */ | |
| public function scan( $args, $assoc_args ) { | |
| WP_CLI::log( 'Scanning for orphaned ACF meta...' ); | |
| $orphans = scan_orphaned_acf_meta(); | |
| if ( empty( $orphans ) ) { | |
| WP_CLI::success( 'No orphaned ACF meta found!' ); | |
| return; | |
| } | |
| WP_CLI::log( sprintf( 'Found %d orphaned entries:', count( $orphans ) ) ); | |
| // Display sample orphans | |
| $sample = array_slice( $orphans, 0, 10 ); | |
| foreach ( $sample as $orphan ) { | |
| WP_CLI::log( sprintf( | |
| ' - Post ID: %d, Meta Key: %s', | |
| $orphan->post_id, | |
| $orphan->meta_key | |
| ) ); | |
| } | |
| if ( count( $orphans ) > 10 ) { | |
| WP_CLI::log( sprintf( ' ... and %d more', count( $orphans ) - 10 ) ); | |
| } | |
| WP_CLI::warning( 'Run "wp acf-cleaner clean" to remove these entries.' ); | |
| } | |
| /** | |
| * Clean orphaned ACF meta | |
| * | |
| * ## OPTIONS | |
| * | |
| * [--yes] | |
| * : Skip confirmation | |
| * | |
| * ## EXAMPLES | |
| * | |
| * wp acf-cleaner clean --yes | |
| */ | |
| public function clean( $args, $assoc_args ) { | |
| WP_CLI::confirm( 'This will permanently delete orphaned ACF meta. Continue?', $assoc_args ); | |
| WP_CLI::log( 'Cleaning orphaned ACF meta...' ); | |
| $results = clean_orphaned_acf_meta( false ); | |
| if ( $results['status'] === 'success' ) { | |
| WP_CLI::success( sprintf( 'Cleaned %d orphaned entries!', $results['count'] ) ); | |
| } else { | |
| WP_CLI::error( $results['message'] ); | |
| } | |
| } | |
| } | |
| WP_CLI::add_command( 'acf-cleaner', 'ACF_Cleaner_Command' ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment