Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions cleanup-orphaned-page-folders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---

title: "Clean up orphaned page folders and their content"

---

version: 1.0.0

---

authors: Olaf Gleba

---

tags: pages, files

---

date: 2026-01-25

---



## Problem

For your new project you duplicate a old installation so you don't have to start from scratch. While building the new website your `assets/files` folder gets messy,- there are a growing number of orphaned page id folders with obsolete content you want to get rid of.

## Solution

Create a new document, copy&paste the code below, save it to the root of your PW installation and open it in a browser. Initially it performs a dry run, so your are save before actual deletion take place.

Because we are dealing with actual files, make a backup of you `files` folder and double check the output results before changing the mode.

```php
<?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
*
* Usage:
*
* Place the script file in the root of your PW installation (or
* adapt the pw bootstrap path), than open the file in 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);
}
```
220 changes: 220 additions & 0 deletions swap-multilanguage-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---

title: "Swap the content of multilingual fields between two languages"

---

version: 1.0.0

---

authors: Olaf Gleba

---

tags: pages, languages, multilingual, multilanguages

---

date: 2026-01-25

---



## Problem

On Launch the defined default language of a website is e.g. `german` (`/`). Beside e.g. `english` as a second language (`/en/`). After a while eventually the client wants to have `english` as the default language and `german` as second.

Usually this gives the editorial staff a really hard time, because every bit of multilingual content has to be replaced by hand. Beside approaches that imposes redirects.


## Solution

The script iterates recursively over multilingual fields of different types and swap the content between two languages.

Create a new document, copy&paste the code below, save it to the root of your PW installation, adapt params (s. instructions within the script file) and open it in a browser.

```php
<?php namespace ProcessWire;

/**
* Swap the content of multilingual fields between two languages
*
* - Tested with ProcessWire 3.0.x, PHP 8.4.x
*
* Usage:
*
* Place the script file in the root of your PW installation (or
* adapt the pw bootstrap path), adapt params (s. below), than open
* the file in the browser.
*
* @author Olaf Gleba
* @version 1.0.0
*/

/**
* USAGE, VERBOSE
*
* 1. Place the (uncommented) `Code example` at the end of this script file
* 2. Adapt `<your-language-name>` with the name of the language that should
* become default.
* 2. Adapt `<your-template-name>` with the required template names
* (assuming that multiple templates are to consider). Execute the script
* file (e.g. reload the browser) ``each time`` you adapt the template name.
*
* Code example:
*
* $_pages = wire('pages')->find("template=<your-template-name>");
* swapMultiLanguageContent($_pages, 'default', '<your-language-name>');
*
*/

/**
* MANDATORY STEPS AFTER FINAL EXECUTION
*
* After you run all considered templates you need to do:
*
* 1. Assuming `german` was your default language so far (name `default`)
* and you want to define a second language, for example `english`
* (name `english`), as the new default, adapt the name (and surely the
* title too) on both related languages (e.g. english => default,
* german => german).
*
* 2. Go to your Homepage ("/") and in the settings tab, adapt the url of your
* new default language and the url of your swapped language (e.g. english
* => /, german => /de/).
*/


// bootstrap processwire
require_once './index.php';


/**
* Function to swap the content of multilingual fields between two languages
*
* 1. Page name swap
* 2. Execute recursive function `swapFieldValuesRecursive`
* 3. Save pages array
*/

function swapMultiLanguageContent($pages, $langA = 'default', $langB = 'deutsch') {

$languages = wire('languages');
$lA = $languages->get($langA);
$lB = $languages->get($langB);

if(!$lA || !$lB) throw new WireException("Language not found");

foreach($pages as $page) {

if(!$page instanceof Page || !$page->id) continue;

$page->of(false);

/* 1 */
try {
$nameA = $page->localName($lA);
$nameB = $page->localName($lB);

if($nameA !== $nameB) {

$page->set("status$langB", 1);

$page->set("name$langA", $nameB);
$page->set("name$langB", $nameA);
}
} catch(Exception $e) {
wire('log')->save('swap-lang', "Page {$page->id}: Name swap error: " . $e->getMessage());
}

/* 2 */
swapFieldValuesRecursive($page, $lA, $lB);

/* 3 */
try {
$page->save();
} catch(Exception $e) {
wire('log')->save('swap-lang', "Page {$page->id}: Save error: " . $e->getMessage());
}

$page->of(true);
}

return "Done.";
}


/**
* Internal function to swap field values recursively
*
* NOT intended to use as stand-alone, s. func `swapMultiLanguageContent`
*
* 1. Ordinary field type
* 2. Repeater field type
* 3. RepeaterMatrix field type
*/

function swapFieldValuesRecursive(Page $page, Language $lA, Language $lB) {

foreach($page->template->fieldgroup as $field) {

$fieldname = $field->name;
if(!$page->hasField($fieldname)) continue;

$value = $page->$fieldname;

if($value === null) continue;

/* 1 */
$type = $field->type;

if($type instanceof FieldtypeLanguageInterface) {

try {
$valueA = $value->getLanguageValue($lA);
$valueB = $value->getLanguageValue($lB);

if($valueA != $valueB) {
$value->setLanguageValue($lA, $valueB);
$value->setLanguageValue($lB, $valueA);
}

} catch(Exception $e) {
wire('log')->save('swap-lang', "Field {$fieldname} error: " . $e->getMessage());
}

continue;
}

/* 2 */
if($type instanceof FieldtypeRepeater && $value->count()) {

foreach($value as $repItem) {
$repItem->of(false);
swapFieldValuesRecursive($repItem, $lA, $lB);
$repItem->save();
$repItem->of(true);
}

continue;
}

/* 3 */
if($type instanceof FieldtypeRepeaterMatrix && $value->count()) {

foreach($value as $matrixItem) {
$matrixItem->of(false);
swapFieldValuesRecursive($matrixItem, $lA, $lB);
$matrixItem->save();
$matrixItem->of(true);
}

continue;
}

// Ignore all other field types...
}
}
```