Skip to content

Instantly share code, notes, and snippets.

@krasenslavov
Last active December 8, 2025 07:27
Show Gist options
  • Select an option

  • Save krasenslavov/888821219e43cdfc85c7288205a7102d to your computer and use it in GitHub Desktop.

Select an option

Save krasenslavov/888821219e43cdfc85c7288205a7102d to your computer and use it in GitHub Desktop.
ACF Cleaner: Remove Orphaned Custom Field Data in WordPress
<?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 );
}
<?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;
}
<?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'
);
} );
<?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;
}
<?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;
}
<?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' );
<?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