Skip to content
Open
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
93 changes: 56 additions & 37 deletions commands/clean/content.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
declare(strict_types = 1);

use Kirby\CLI\CLI;
use Kirby\Cms\Languages;

$cleanContent = function (
CLI $cli,
Generator $collection,
array|null $ignore = null,
string|null $lang = null
string $lang = 'default',
Copy link
Member

@afbora afbora Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though we only send a language object for one language, why do we accept default as a fallback? Do you think there's a case where this parameter called with default argument?

bool $dryrun = false,
): void {
foreach ($collection as $item) {
// get all fields in the content file
Expand All @@ -22,69 +25,85 @@

// get the keys and normalize to lowercase
$originalContentKeys = array_keys($contentFields);
$contentFieldKeys = array_map('mb_strtolower', $originalContentKeys);
$contentFieldKeys = array_map('mb_strtolower', $originalContentKeys);

// get all field keys from blueprint and normalize to lowercase
$blueprintFields = array_keys($item->blueprint()->fields());
$blueprintFields = array_keys($item->blueprint()->fields());
$blueprintFieldKeys = array_map('mb_strtolower', $blueprintFields);

// get all field keys that are in $contentFieldKeys but not in $blueprintFieldKeys
$fieldsToBeDeleted = array_diff($contentFieldKeys, $blueprintFieldKeys);

// update page only if there are any fields to be deleted
if (count($fieldsToBeDeleted) > 0) {
// create a mapping: lowercase => original field name
$lowercaseToOriginal = array_combine($contentFieldKeys, $originalContentKeys);

// build data array with original field names as keys and null as values
$data = [];
foreach ($fieldsToBeDeleted as $lowercaseField) {
$originalField = $lowercaseToOriginal[$lowercaseField];
$data[$originalField] = null;
}
if (count($fieldsToBeDeleted) === 0) {
continue;
}

// get the latest version of the item
$version = $item->version('latest');
// create a mapping: lowercase => original field name
$lowercaseToOriginal = array_combine($contentFieldKeys, $originalContentKeys);

// check if the version exists for the given language
// and try to update the page with the data
if ($version->exists($lang ?? 'default') === true) {
$version->update($data, $lang ?? 'default');
}
// build data array with original field names as keys and null as values
$data = [];

foreach ($fieldsToBeDeleted as $lowercaseField) {
$originalField = $lowercaseToOriginal[$lowercaseField];
$data[$originalField] = null;

$cli->out('Remove "' . $originalField . '" from ' . $item::CLASS_ALIAS . ' (' . $item->id() . ')');
}

// don't update models that have changes
if ($item->version('changes')->exists($lang) === true) {
$cli->error('The ' . $item::CLASS_ALIAS . ' (' . $item->id() . ') has changes and cannot be cleaned. Save the changes and try again.');
}

// in a dry-run, the models will not be updated
if ($dryrun === true) {
continue;
}

// get the latest version of the item
$version = $item->version('latest');

// check if the version exists for the given language
// and try to update the page with the data
if ($version->exists($lang) === true) {
$version->update($data, $lang);
}
}
};

return [
'description' => 'Deletes all fields from page, file or user content files that are not defined in the blueprint, no matter if they contain content or not.',
'args' => [
'dry-run' => [
'description' => 'Run the command without actually updating content',
'noValue' => true,
],
],
'command' => static function (CLI $cli) use ($cleanContent): void {

$cli->confirmToContinue('This will delete all fields from content files that are not defined in blueprints, no matter if they contain content or not. Are you sure?');
$kirby = $cli->kirby();
$dryrun = $cli->arg('dry-run');

$kirby = $cli->kirby();
if ($dryrun === false) {
$cli->confirmToContinue('This will delete all fields from content files that are not defined in blueprints, no matter if they contain content or not. Are you sure?');
}

// Authenticate as almighty
$kirby->impersonate('kirby');

// set the fields to be ignored
$ignore = ['uuid', 'title', 'slug', 'template', 'sort', 'focus'];

// call the script for all languages if multilang
if ($kirby->multilang() === true) {
$languages = $kirby->languages();

foreach ($languages as $language) {
// should call kirby models for each loop
// since generators cannot be cloned
// otherwise run into an exception
$collection = $kirby->models();

$cleanContent($collection, $ignore, $language->code());
}

} else {
// go through all languages
foreach (Languages::ensure() as $language) {
// should call kirby models for each loop
// since generators cannot be cloned
// otherwise run into an exception
$collection = $kirby->models();
$cleanContent($collection, $ignore);

$cleanContent($cli, $collection, $ignore, $language->code(), $dryrun);
}

$cli->success('The content files have been cleaned');
Expand Down
Loading