index-main/kirby/src/Cms/PageCopy.php

237 lines
5.5 KiB
PHP
Raw Normal View History

2025-10-03 07:46:23 +02:00
<?php
namespace Kirby\Cms;
use Kirby\Uuid\Uuid;
use Kirby\Uuid\Uuids;
/**
* Normalizes a newly generated copy of a page,
* adapting page slugs, UUIDs etc.
* (for single as well as multilang setups)
*
* @package Kirby Cms
* @author Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
* @since 5.0.0
* @unstable
*/
class PageCopy
{
public function __construct(
public Page $copy,
public Page|null $original = null,
public bool $withFiles = false,
public bool $withChildren = false,
public array $uuids = []
) {
}
/**
* Converts UUIDs for copied pages,
* replacing the old UUID with a newly generated one
* for all newly generated pages and files
*/
public function convertUuids(Language|null $language): void
{
if (Uuids::enabled() === false) {
return;
}
if (
$language instanceof Language &&
$language->isDefault() === false
) {
return;
}
// store old UUID
$old = $this->copy->uuid()->toString();
// re-generate UUID for the page
$this->copy = $this->copy->save(
['uuid' => Uuid::generate()],
$language?->code()
);
// track UUID change
$this->uuids[$old] = $this->copy->uuid()->toString();
$this->convertFileUuids($language);
$this->convertChildrenUuids($language);
}
/**
* Re-generate UUIDs for each child recursively
* and merge with the tracked changed UUIDs
*/
protected function convertChildrenUuids(Language|null $language): void
{
// re-generate UUIDs and track changes
if ($this->withChildren === true) {
foreach ($this->copy->childrenAndDrafts() as $child) {
// always adapt files of subpages as they are
// currently always copied; adapt children recursively
$child = new PageCopy(
$child,
withChildren: true,
withFiles: true,
uuids: $this->uuids
);
$child->convertUuids($language);
$this->uuids = [...$this->uuids, ...$child->uuids];
}
}
// if children have not been copied over,
// track all children UUIDs from original page to
// remove/replace with empty string
if ($this->withChildren === false) {
foreach ($this->original?->index(drafts: true) ?? [] as $child) {
$this->uuids[$child->uuid()->toString()] = '';
foreach ($child->files() as $file) {
$this->uuids[$file->uuid()->toString()] = '';
}
}
}
}
/**
* Re-generate UUID for each file and track the change
*/
protected function convertFileUuids(Language|null $language): void
{
// re-generate UUIDs and track changes
if ($this->withFiles === true) {
foreach ($this->copy->files() as $file) {
// store old file UUID
$old = $file->uuid()->toString();
// re-generate UUID for the file
$file = $file->save(
['uuid' => Uuid::generate()],
$language?->code()
);
// track UUID change
$this->uuids[$old] = $file->uuid()->toString();
}
}
// if files have not been copied over,
// track file UUIDs from original page to
// remove/replace with empty string
if ($this->withFiles === false) {
foreach ($this->original?->files() ?? [] as $file) {
$this->uuids[$file->uuid()->toString()] = '';
}
}
}
/**
* Returns all languages to adapt
*
* @todo Refactor once singe-lang mode also works with a language object
*/
public function languages(): Languages|iterable
{
$kirby = App::instance();
if ($kirby->multilang() === true) {
return $kirby->languages();
}
return [null];
}
/**
* Processes the copy with all necessary adaptations.
* Main method to use if not familiar with individual steps.
*/
public static function process(
Page $copy,
Page|null $original = null,
bool $withFiles = false,
bool $withChildren = false
): Page {
$converter = new static($copy, $original, $withFiles, $withChildren);
// loop through all languages to remove slug from non-default
// languages and re-generate UUIDs (and track changes)
foreach ($converter->languages() as $language) {
$converter->removeSlug($language);
$converter->convertUuids($language);
}
// apply all tracked UUID changes at once
$converter->replaceUuids();
return $converter->copy;
}
/**
* Removes translated slug for copied page.
* This is needed to avoid translated slug
* collisions with the original page.
*/
public function removeSlug(Language|null $language): void
{
// single lang setup
if ($language === null) {
return;
}
// don't remove slug from default language
if ($language->isDefault() === true) {
return;
}
if ($this->copy->translation($language)->exists() === true) {
$this->copy = $this->copy->save(
['slug' => null],
$language->code()
);
}
}
/**
* Replace old UUIDs with new UUIDs in the content
*/
public function replaceUuids(): void
{
if (Uuids::enabled() === false) {
return;
}
foreach ($this->copy->storage()->all() as $versionId => $language) {
$this->copy->storage()->replaceStrings(
$versionId,
$language,
$this->uuids
);
}
if ($this->withFiles === true) {
foreach ($this->copy->files() as $file) {
foreach ($file->storage()->all() as $versionId => $language) {
$file->storage()->replaceStrings(
$versionId,
$language,
$this->uuids
);
}
}
}
if ($this->withChildren === true) {
foreach ($this->copy->childrenAndDrafts() as $child) {
$child = new PageCopy($child, withFiles: true, withChildren: true, uuids: $this->uuids);
$child->replaceUuids();
}
}
}
}