Skip to content

Instantly share code, notes, and snippets.

@olafgleba
Last active January 25, 2026 23:01
Show Gist options
  • Select an option

  • Save olafgleba/20b15d3254ab781cd305ee1a45716abf to your computer and use it in GitHub Desktop.

Select an option

Save olafgleba/20b15d3254ab781cd305ee1a45716abf to your computer and use it in GitHub Desktop.
Clean up orphaned page folders in ProcessWire
<?php namespace ProcessWire;
/**
* Cleanup orphaned page folders and their content
*
* - Only delete page folders WITH NO existing Page-ID.
* - Implements a dry run mode
* - Tested with ProcessWire 3.0.x, PHP 8.4.x
*
* Place the script file in the root of your PW installation (or
* adapt the pw bootstrap path), than open the file within the browser.
*
* @author Olaf Gleba
* @version 1.0.0
*/
// Bootstrap ProcessWire
require_once './index.php';
// Define mode (default `dry run`), set to `false` for actual deletion
$dryRun = true;
// Print to browser
echo "<p>Orphaned page folder cleanup</p>";
echo "<p>Mode: " . ($dryRun ? "DRY RUN - NO DELETION" : "LIVE - ACTUAL DELETION") . "</p>";
/**
* Load existing Page-Ids from database
*/
$existingPageIds = [];
$sql = $database->query("SELECT id FROM pages");
while ($row = $sql->fetch(\PDO::FETCH_ASSOC)) {
$existingPageIds[(int)$row['id']] = true;
}
// Print to browser
echo "<p>Existing pages loaded: " . count($existingPageIds) . "</p>";
/**
* Iterate over the `files` folder
*/
// Get files root
$filesRoot = realpath($config->paths->files);
$removedFolders = 0;
$dirs = scandir($filesRoot);
foreach ($dirs as $dir) {
// Only nummeric folders (Page-IDs)
if (!ctype_digit($dir)) {
continue;
}
$pageId = (int)$dir;
$pageDir = $filesRoot . DIRECTORY_SEPARATOR . $dir;
if (!is_dir($pageDir)) {
continue;
}
// Skip if page exists
if (isset($existingPageIds[$pageId])) {
continue;
}
// Print to browser
echo "<p>Orphaned page folder: {$pageDir}</p>";
// List folder files
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$pageDir,
\FilesystemIterator::SKIP_DOTS
),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item) {
if ($item->isFile()) {
echo " File: " . $item->getRealPath() . "<br />";
}
}
// Delete folder (if args `--delete` is present, s.above)
if (!$dryRun) {
deleteDirectory($pageDir);
echo " FOLDER DELETED<br />";
} else {
echo " (DRY RUN - NO DELETION)<br />";
}
echo "<br/>";
$removedFolders++;
}
/**
* Print summary to browser
*/
echo "<p>";
echo "---------------------------------<br />";
echo "Done<br />";
echo "Orphaned page folders: {$removedFolders}<br />";
echo "---------------------------------<br />";
echo "</p>";
/**
* Recursive deletion
*/
function deleteDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
deleteDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment