D
P
0

WordPress & PHP

Cleanup Script Dies With `Call to undefined function` on `require 'wp-load.php'`? Wipe WordPress via Raw `mysqli` Instead

June 30, 2026·5 min read
Cleanup Script Dies With `Call to undefined function` on `require 'wp-load.php'`? Wipe WordPress via Raw `mysqli` Instead

A WordPress install I was responsible for had just been contaminated: a botched seeding run had injected thousands of junk posts, orphaned postmeta rows, and wrong term relationships across the entire database. All of it had to go, in bulk. My plan was standard and boring, exactly the way a plan should be: drop a small cleanup script in the install root, boot WordPress, and use its API so every hook fires and data integrity stays intact.

<?php
// cleanup.php - the obvious route
require __DIR__ . '/wp-load.php';
 
// ...WP_Query, wp_delete_post(), done. In theory.

The script never made it past the first line.

PHP Fatal error: Uncaught Error: Call to undefined function
render_listing_badge() in wp-content/themes/active-theme/functions.php on line 27

I opened the active theme's functions.php, and there it was: the file called several helper functions it did not own. Those helpers had been provided by a code-snippets plugin, and that plugin had been disabled during the incident. The consequence went far beyond my script. EVERY WordPress bootstrap path — wp-load.php, wp-admin, even the front-end — died at the same spot, before a single line of my code could run. WP-CLI was not available on that host, so --skip-themes was not an option either.

The catch-22

The situation is funny as long as it is not happening to you. I needed the WordPress API to clean the site, but that API only exists once the site boots, and the site could not boot precisely because it was dirty. My cleanup tool depended on the very thing it was supposed to clean.

Framed that way, the way out was obvious: do not boot WordPress at all. Everything I actually needed came down to two things, the database credentials and the table names, and both are obtainable without executing a single line of WordPress code.

Step zero: dump first

Before touching anything, dump the database. This is not optional; every step after this one is destructive.

mysqldump -u backup_user -p wordpress_db > backup-before-cleanup.sql

Read wp-config.php as text, never execute it

The first reflex is require 'wp-config.php'; to get the DB constants. Do not. The last line of wp-config.php is a require_once of wp-settings.php, which triggers the exact same full bootstrap and the exact same fatal. The file has to be treated as plain text and parsed with regex:

$config = file_get_contents(__DIR__ . '/wp-config.php');
 
function config_value(string $config, string $key): string
{
    $pattern = "/define\(\s*['\"]" . $key . "['\"]\s*,\s*['\"](.*?)['\"]\s*\)/";
    if (!preg_match($pattern, $config, $m)) {
        throw new RuntimeException("$key not found in wp-config.php");
    }
    return $m[1];
}
 
$dbName = config_value($config, 'DB_NAME');
$dbUser = config_value($config, 'DB_USER');
$dbPass = config_value($config, 'DB_PASSWORD');
$dbHost = config_value($config, 'DB_HOST');
 
preg_match('/\$table_prefix\s*=\s*[\'"](.+?)[\'"]/', $config, $m);
$prefix = $m[1];

Do not skip $table_prefix. Assuming the prefix is wp_ is the classic way to run a DELETE against a table that does not exist, or worse, against the wrong table on a multi-site-in-one-database setup.

Raw mysqli: dry-run first, transaction always

With credentials in hand, the rest is plain SQL over mysqli. The rule I held onto hard: every destructive query gets a SELECT COUNT twin that runs first, with an identical WHERE clause. If the number looks off, stop.

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
 
$count = $db->query(
    "SELECT COUNT(*) AS total FROM {$prefix}posts
     WHERE post_type = 'listing'"
)->fetch_assoc()['total'];
 
echo "Would delete {$count} posts. Ctrl+C now if that looks wrong.\n";

The count matched my estimate, so on to the real deletions, wrapped in a transaction (the tables were InnoDB, so a rollback actually means something):

$db->begin_transaction();
 
$db->query(
    "DELETE pm FROM {$prefix}postmeta pm
     JOIN {$prefix}posts p ON p.ID = pm.post_id
     WHERE p.post_type = 'listing'"
);
$db->query(
    "DELETE tr FROM {$prefix}term_relationships tr
     JOIN {$prefix}posts p ON p.ID = tr.object_id
     WHERE p.post_type = 'listing'"
);
$db->query(
    "DELETE FROM {$prefix}posts WHERE post_type = 'listing'"
);
 
$db->commit();

The order is deliberate: meta and term relationships go while the parent posts still exist to JOIN against, and the posts themselves go last.

Switch the theme at the DB level before touching theme data

Some of the contaminated data was theme-related, and this is where one decision mattered most: the fatal itself came from the active theme's functions.php. As long as that theme stayed active, the site would never boot, no matter how clean the database got. So before touching anything theme-related, I switched the active theme to a default one directly in the options table:

$db->query(
    "UPDATE {$prefix}options
     SET option_value = 'twentytwentyfive'
     WHERE option_name IN ('template', 'stylesheet')"
);

Those two options, template and stylesheet, are the only thing that determines the active theme. The moment that UPDATE ran, wp-admin loaded again, because the offending functions.php was never loaded.

Verify, then turn things back on one by one

With the cleanup done, I opened wp-admin: login fine, no fatals, content counts as expected. Only then did I reactivate components one at a time, plugins first with a check after each, and the old theme dead last, and only after its missing helpers had been moved somewhere they belong. Not back into a code-snippets plugin.

Checklist

  • A cleanup tool must not depend on the thing it is cleaning. If the bootstrap is broken, do not go through the bootstrap.
  • Regex-parse wp-config.php as text. Including it is the same as booting WordPress.
  • Take $table_prefix from the config, never assume wp_.
  • mysqldump before the first destructive query.
  • Every DELETE or UPDATE gets a SELECT COUNT twin that runs first.
  • Switch the active theme at the DB level before touching theme data, so the site can boot again afterwards.
  • Reactivate one by one, verifying between steps, never all at once.

Since that incident, whenever I write a repair script, my first question is no longer "which API do I need" but "does this script still run when the patient is flatlining".