Files
WP-LIT-MCP/src/Integrations/Divi/Library/LibraryMakeGlobalAbility.php
T
janniclas.loosenandClaude Opus 5.5 a8afa37208 fix: make-global keeps the backup sentence on a failure on page 2+ (ruling P14)
failedOn() replaced the hint for a failure after earlier pages were
replaced, so the "Sicherung X liegt im Papierkorb" sentence of the failed
page's backup was lost. The hint now leads with it when a backup exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-27 23:25:53 +02:00

671 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use Closure;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\BlockTree\Block;
use LIT\MCP\Integrations\Divi\BlockTree\Fingerprint;
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent;
use LIT\MCP\Integrations\Divi\BlockTree\MalformedMarkup;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\Pages\DiviChange;
use LIT\MCP\Integrations\Divi\Pages\DiviErrors;
use LIT\MCP\Integrations\Divi\Pages\DiviPage;
use LIT\MCP\Integrations\Divi\Pages\DiviSchema;
use LIT\MCP\Integrations\Divi\Pages\DiviWriter;
use LIT\MCP\Integrations\Divi\Pages\VisibleEdits;
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\PostSaveFollowUp;
use LIT\MCP\Log;
use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/library-make-global (spec §5.7, E10): a node becomes a global module – a new global library entry holding
* exactly its bytes (NewLibraryEntry, the lit/library-save path: rights with scope "global", ruling P7,
* StoredNodeGuard, no reference to any entry in any spelling, ruling Q7) and the node replaced on its page by the
* instance the Visual Builder writes (GlobalInstance::of). All or nothing: the page is written through
* DiviWriter::commit() (tree_rev lock, revision); when it is refused or rolled back, the new entry is deleted
* again (NewDraft) – only a page write that may have been saved (partial) keeps the entry, because the page may
* already show it. Sources are pages and posts only (post types page and post, ruling Q25): no library entries
* (no nesting, ruling P11), no Theme Builder layouts (ruling Q24; there lit/library-save and lit-library-insert)
* and no other Divi-enabled types. Needs the rights of
* lit/library-save with scope "global" and of lit-library-insert, and follows lit-divi-insert (audit M2).
*
* Second mode (E10, answer F2): library_id plus targets replaces further occurrences – only nodes whose content
* fingerprint and normalized bytes (Fingerprint::canonical, ruling Q25: visible text parts included) equal the
* entry's root ("identical" in lit/library-find-similar) – by instances of that active
* global entry (one readable root, no reference to any entry in any spelling, ruling Q7). A global edit: every
* target is checked first (edit_post and tree_rev through DiviWriter::page(), P8a – the account changes only pages
* it may edit; identical nodes; the instance passes the page's StoredNodeGuard; post types page and post only),
* then a dry run lists the pages (tree_rev, status, live – drafts are listed, like P8a), and only confirm: true
* writes. Every page goes through DiviWriter::commit() with its own tree_rev
* lock, revision and change-log row; a live page (publish, private, future) without revisions first gets
* VisibleEdits' backup, refused for every page before anything is written where it could not be faithful (P9).
* Pages are written in the given order; when page N fails, pages 1..N-1 stay replaced (each with its restore
* point), page N is rolled back by DiviSaver (or reported partial), pages after N are not attempted – the error
* names all three groups (changed_posts with pages, failed_post, unchanged_posts; D10, S10).
*/
final class LibraryMakeGlobalAbility implements Ability
{
public const REASON_PAGE = 'Die Seite konnte nicht gespeichert werden; der neue globale Eintrag wurde wieder '
. 'entfernt.';
public const MAX_TARGETS = 20;
public const MAX_IDS = 20;
private const TYPE_LABELS = ['section' => 'Abschnitt', 'row' => 'Zeile', 'module' => 'Modul'];
/** Post types that are sources and targets (ruling Q25): pages and posts, no other Divi-enabled type. */
public const POST_TYPES = ['page', 'post'];
/** Inputs of the first mode, refused next to library_id. */
private const NODE_INPUT = ['post_id', 'tree_rev', 'id', 'title', 'categories', 'tags'];
private VisibleEdits $visible;
/**
* @param (Closure(): bool)|null $diviWritable Whether lit/divi-insert is enabled (audit M2); null = always.
* @param VisibleEdits|null $visible Backup of live pages without revisions; null = the default.
*/
public function __construct(
private LibraryAccess $access,
private DiviWriter $writer,
private NewLibraryEntry $entries,
private ChangeLog $log,
private ?Closure $diviWritable = null,
?VisibleEdits $visible = null,
private Parser $parser = new Parser()
) {
$this->visible = $visible ?? new VisibleEdits();
}
public function name(): string
{
return 'lit/library-make-global';
}
public function label(): string
{
return 'In globales Modul umwandeln';
}
public function description(): string
{
return 'Wandelt einen Divi-Knoten (post_id, tree_rev und id aus lit-divi-outline, title) auf einer Seite oder '
. 'in einem Beitrag (Inhaltstypen page und post) in ein globales Modul um: legt einen globalen '
. 'Bibliothekseintrag mit genau diesem Inhalt an und ersetzt den Knoten durch die verknüpfte Instanz – '
. 'alles oder nichts. Danach zeigt die '
. 'Seite den Inhalt aus der Bibliothek; Änderungen dort wirken auf alle Seiten mit diesem Modul.'
. ' Gleichartige Vorkommen danach ersetzen: library_id plus targets (je post_id, tree_rev und ids '
. 'inhaltsgleicher Knoten aus lit/library-find-similar, match "identical"; nur Seiten und Beiträge); das '
. 'ist zuerst ein Probelauf mit den betroffenen Seiten und braucht dann confirm: true. Jede Seite bekommt '
. 'eine Revision.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => array_merge(DiviSchema::write(), [
'id' => ['type' => 'string', 'description' => 'Pfad-ID des Knotens, z. B. "s1".'],
'title' => ['type' => 'string', 'minLength' => 1, 'maxLength' => 200],
'categories' => ['type' => 'array', 'maxItems' => 10, 'items' => ['type' => 'string']],
'tags' => ['type' => 'array', 'maxItems' => 10, 'items' => ['type' => 'string']],
'library_id' => [
'type' => 'integer',
'minimum' => 1,
'description' => 'Globaler Eintrag, dessen inhaltsgleiche Vorkommen ersetzt werden (mit targets).',
],
'targets' => [
'type' => 'array',
'minItems' => 1,
'maxItems' => self::MAX_TARGETS,
'items' => [
'type' => 'object',
'properties' => [
'post_id' => ['type' => 'integer', 'minimum' => 1],
'tree_rev' => ['type' => 'string'],
'ids' => [
'type' => 'array',
'minItems' => 1,
'maxItems' => self::MAX_IDS,
'items' => ['type' => 'string'],
],
],
'required' => ['post_id', 'tree_rev', 'ids'],
'additionalProperties' => false,
],
],
]),
'required' => [],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return ToolResult::schema([
'library_id' => ['type' => 'integer'],
'tree_rev' => ['type' => 'string'],
'pages' => ['type' => 'array', 'items' => ['type' => 'object']],
], ['library_id']);
}
public function annotations(): array
{
return Annotations::destructive(false);
}
public function isDirect(): bool
{
return false;
}
public function permission(array $input): bool|WP_Error
{
return $this->precheck($input) ?? true;
}
/** @throws WriteFailed */
public function execute(array $input): mixed
{
$refusal = $this->precheck($input);
if ($refusal !== null) {
return $refusal;
}
return self::replaces($input) ? $this->replaceOccurrences($input) : $this->makeGlobal($input);
}
/** @param array<string, mixed> $input Whether the call replaces occurrences (second mode). */
private static function replaces(array $input): bool
{
return array_key_exists('library_id', $input) || array_key_exists('targets', $input);
}
/**
* Rights of lit-library-insert (add_library, library group writable) and – for a new entry only – of
* lit/library-save with scope "global" (publish_posts, save_library, edit_global_library), and
* lit-divi-insert's Tools-tab state (M2).
*
* @param array<string, mixed> $input
*/
private function precheck(array $input): ?WP_Error
{
$refusal = $this->access->canInsert() ?? (self::replaces($input) ? null : $this->access->canSave(true));
if ($refusal !== null) {
return $refusal;
}
if ($this->diviWritable !== null && ! ($this->diviWritable)()) {
return Errors::unsupported(
'Divi-Änderungen sind für diese Verbindung abgeschaltet (Gruppe „Divi“ auf „nur lesen“ oder '
. 'lit-divi-insert deaktiviert).',
'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.'
);
}
return null;
}
/**
* @param array<string, mixed> $input
* @return array<string, mixed>|WP_Error
* @throws WriteFailed
*/
private function makeGlobal(array $input): array|WP_Error
{
$page = $this->writer->page($input);
if ($page instanceof WP_Error) {
return $page;
}
$id = is_string($input['id'] ?? null) ? $input['id'] : '';
$node = $page->index->find($id);
if ($node === null) {
return DiviErrors::unknownNode($id, $page->post);
}
$refusal = self::sourceRefusal($page);
if ($refusal !== null) {
return $refusal;
}
// LibraryItemSpec::forNode() refuses instances and nodes holding one, NewLibraryEntry every other
// spelling of a reference (rulings P11, Q7, B17) – one rule, no copy here.
$spec = $this->entries->spec($page, $node, $input, true, $this->writer->composition($page)->storedGuard());
if ($spec instanceof WP_Error) {
return $spec;
}
$draft = new NewDraft(null, $this->log, $this->name(), DiviLibraryStore::POST_TYPE, 'Bibliothekseintrag');
$created = $this->entries->create($spec, $node, $draft);
$libraryId = $created['id'];
$root = $node->copy();
$root->unifyLineBreaks();
$instance = GlobalInstance::of($root, $libraryId);
$page->index->parentOf($node)?->replaceChild($node, $instance);
$change = (new DiviChange(sprintf(
'Knoten %s in das globale Modul „%s“ (%d) umgewandelt',
$id,
$spec->title,
$libraryId
)))->record($instance, 'replaced');
try {
$result = $this->writer->commit($this->name(), $page, $change, $input);
} catch (WriteFailed $e) {
if (! $e->rolledBack) {
// The page may already show the instance: deleting the entry would leave it empty.
throw $e->withDetails(
['library_id' => $libraryId],
sprintf(
'Den Aufruf nicht einfach wiederholen. Der neue globale Eintrag %d bleibt bestehen, weil die '
. 'Seite ihn möglicherweise schon zeigt; den Stand mit lit-divi-outline prüfen und dem '
. 'Nutzer Bescheid geben.',
$libraryId
)
);
}
$draft->discard($libraryId, $e, self::REASON_PAGE);
}
if ($result instanceof WP_Error) {
return $draft->refuse($libraryId, $result);
}
return $this->result($result, $spec, $libraryId, $created['warnings'], $page, $input);
}
/**
* @param array<string, mixed> $input
* @return array<string, mixed>|WP_Error
* @throws WriteFailed
*/
private function replaceOccurrences(array $input): array|WP_Error
{
$mixed = array_values(array_intersect(self::NODE_INPUT, array_keys($input)));
if ($mixed !== [] || ! array_key_exists('library_id', $input) || ! array_key_exists('targets', $input)) {
return Errors::invalidInput(
'Zum Ersetzen library_id und targets angeben (ohne post_id, id und title); nichts wurde gespeichert.',
['library_id' => 'library_id und targets zusammen, ohne ' . implode(', ', $mixed ?: self::NODE_INPUT)]
);
}
$root = $this->globalRoot($input['library_id']);
if ($root instanceof WP_Error) {
return $root;
}
[$libraryId, $block] = $root;
$plans = $this->plans($libraryId, $block, $input['targets']);
if ($plans instanceof WP_Error) {
return $plans;
}
$affected = array_map(static fn (array $plan): array => [
'post_id' => (int) $plan[0]->post->ID,
'title' => (string) $plan[0]->post->post_title,
'status' => (string) $plan[0]->post->post_status,
'live' => self::isLive($plan[0]->post),
'tree_rev' => $plan[0]->treeRev(),
'ids' => array_keys($plan[1]),
], $plans);
if (($input['confirm'] ?? false) !== true) {
$live = count(array_filter(array_column($affected, 'live')));
return Errors::confirmRequired(
sprintf(
'%d Vorkommen auf %d %s (%d davon veröffentlicht, privat oder geplant) würden durch das globale '
. 'Modul %d ersetzt; die Seiten zeigen danach den Inhalt aus der Bibliothek. Nichts wurde '
. 'gespeichert (Probelauf).',
array_sum(array_map(static fn (array $row): int => count($row['ids']), $affected)),
count($affected),
count($affected) === 1 ? 'Seite' : 'Seiten',
$live,
$libraryId
),
['affected' => $affected, 'affected_count' => count($affected), 'live_count' => $live]
);
}
foreach ($plans as [$page]) {
$refusal = self::isLive($page->post) ? $this->visible->backupRefusal(
$page->post,
sprintf('Seite %d', (int) $page->post->ID),
'Revisionen aktivieren (WP_POST_REVISIONS) oder die Seite im Divi-Builder bearbeiten.'
) : null;
if ($refusal !== null) {
return $refusal;
}
}
return $this->replace($libraryId, $block, $plans);
}
/**
* Every target checked before anything is written: page rights and tree_rev (DiviWriter::page(), P8a),
* pages and posts only, each once, identical nodes, the instance through the page's StoredNodeGuard.
*
* @return list<array{0: DiviPage, 1: array<string, Block>}>|WP_Error
*/
private function plans(int $libraryId, Block $root, mixed $targets): array|WP_Error
{
$targets = is_array($targets) ? array_values($targets) : [];
if ($targets === [] || count($targets) > self::MAX_TARGETS) {
return Errors::invalidInput(
sprintf('targets fehlt oder hat mehr als %d Seiten; nichts wurde gespeichert.', self::MAX_TARGETS),
['targets' => sprintf('1 bis %d Seiten', self::MAX_TARGETS)]
);
}
$fingerprint = Fingerprint::content($root);
$canonical = Fingerprint::canonical($root);
$plans = [];
$errors = [];
$seen = [];
foreach ($targets as $i => $target) {
$target = is_array($target) ? $target : [];
$page = $this->writer->page([
'post_id' => $target['post_id'] ?? null,
'tree_rev' => $target['tree_rev'] ?? null,
]);
if ($page instanceof WP_Error) {
return $page;
}
$postId = (int) $page->post->ID;
if (self::sourceRefusal($page) !== null || isset($seen[$postId])) {
$errors["targets.$i.post_id"] = 'nur Seiten und Beiträge (page, post), jede einmal (andere Einträge '
. 'einzeln mit lit-library-insert)';
continue;
}
$seen[$postId] = true;
$ids = is_array($target['ids'] ?? null) ? array_values($target['ids']) : [];
if ($ids === [] || count($ids) > self::MAX_IDS) {
$errors["targets.$i.ids"] = sprintf('1 bis %d Pfad-IDs', self::MAX_IDS);
}
$nodes = [];
foreach ($ids as $j => $id) {
$node = is_string($id) ? $page->index->find($id) : null;
// The hash first (cheap), then the normalized bytes themselves (ruling Q25) – no collision counts.
$identical = $node !== null && ! $node->isOpaque() && Fingerprint::content($node) === $fingerprint
&& Fingerprint::canonical($node) === $canonical;
if (! $identical) {
$errors["targets.$i.ids.$j"] = 'kein inhaltsgleiches Vorkommen des Eintrags (nur match "identical" '
. 'aus lit/library-find-similar)';
continue;
}
$nodes[(string) $id] = $node;
}
$guard = $this->writer->composition($page)->storedGuard();
$problems = $guard->problems(GlobalInstance::of($root, $libraryId));
if ($problems !== []) {
$errors["targets.$i.post_id"] = implode('; ', $problems);
}
$plans[] = [$page, $nodes];
}
if ($errors !== []) {
return Errors::invalidInput('Nicht alle Ziele lassen sich ersetzen; nichts wurde gespeichert.', $errors);
}
return $plans;
}
/**
* Writes the pages in order (see the class comment for a failure on page N).
*
* @param list<array{0: DiviPage, 1: array<string, Block>}> $plans
* @return array<string, mixed>
* @throws WriteFailed
*/
private function replace(int $libraryId, Block $root, array $plans): array
{
$pages = [];
$warnings = [];
$changed = [];
foreach ($plans as $n => [$page, $nodes]) {
$postId = (int) $page->post->ID;
$change = new DiviChange(
sprintf('%d Vorkommen durch das globale Modul %d ersetzt', count($nodes), $libraryId)
);
foreach ($nodes as $node) {
$instance = GlobalInstance::of($root, $libraryId);
$page->index->parentOf($node)?->replaceChild($node, $instance);
$change->record($instance, 'replaced');
}
$rest = array_map(static fn (array $plan): int => (int) $plan[0]->post->ID, array_slice($plans, $n + 1));
$backup = [];
try {
$backup = self::isLive($page->post) ? $this->visible->backup($page->post) : [];
$saved = $this->writer->commit(
$this->name(),
$page,
$change,
['post_id' => $postId, 'tree_rev' => $page->treeRev(), 'library_id' => $libraryId]
);
if ($saved instanceof WP_Error) {
throw WriteFailed::rolledBack(Errors::clientMessage($saved), detail: 'commit refused the page');
}
} catch (WriteFailed $e) {
$e = VisibleEdits::keepingBackup($e, $backup['backup_id'] ?? null, 'der Seite');
throw self::failedOn($postId, $e, $pages, $rest);
} catch (Throwable $e) {
Log::failure(sprintf('%s on post %d', $this->name(), $postId), $e);
$unknown = WriteFailed::partial(
'Interner Fehler beim Speichern dieser Seite (Details im PHP-Error-Log der Website).',
$e,
postId: $postId
);
throw self::failedOn($postId, $unknown, $pages, $rest);
}
$pages[] = array_merge([
'post_id' => $postId,
'tree_rev' => (string) ($saved['tree_rev'] ?? ''),
'revision_id' => $saved['revision_id'] ?? null,
], $backup);
$post = $page->post;
$changed[] = [(string) $postId, (string) $post->post_type, 'replaced', (string) $post->post_title];
foreach (is_array($saved['warnings'] ?? null) ? $saved['warnings'] : [] as $warning) {
$warnings[] = sprintf('Eintrag %d: %s', $postId, (string) $warning);
}
}
$tool = ToolResult::ok(sprintf(
'Vorkommen auf %d %s durch das globale Modul %d ersetzt (je mit Revision).',
count($pages),
count($pages) === 1 ? 'Seite' : 'Seiten',
$libraryId
));
foreach ($changed as $entry) {
$tool->changed(...$entry);
}
return $tool->warnings($warnings)
->with('library_id', $libraryId)
->with('pages', $pages)
->toArray();
}
/**
* The failure of page $postId with the state of the whole call: pages already replaced (each with its
* revision or backup), the failed one, the ones not attempted.
*
* @param list<array<string, mixed>> $pages Pages replaced before.
* @param list<int> $rest Pages after the failed one.
*/
private static function failedOn(int $postId, WriteFailed $e, array $pages, array $rest): WriteFailed
{
$done = array_map(static fn (array $row): int => (int) $row['post_id'], $pages);
$details = ['changed_posts' => $done, 'pages' => $pages, 'failed_post' => $postId, 'unchanged_posts' => $rest];
if ($done === []) {
return $e->withDetails($details);
}
// DiviSaver's own state stays (e.g. "Revision X bleibt erhalten").
$state = sprintf(
'Seite %d: %s Bereits ersetzt (je mit Revision): %s; unverändert: %s.',
$postId,
$e->state ?? 'Die Änderung wurde nicht gespeichert.',
implode(', ', $done),
$rest === [] ? 'keine weiteren' : implode(', ', $rest)
);
// Ruling P14: a backup of page N (revisions off) stays named – its sentence leads the hint.
$hint = 'Den Stand mit lit/library-find-similar neu prüfen und nur die übrigen Seiten mit neuem tree_rev '
. 'ersetzen.';
if (isset($e->details['backup_id']) && $e->hint !== null) {
$hint = $e->hint . ' ' . $hint;
}
return $e->withDetails($details, $hint, $e->rolledBack ? $state : null);
}
/** Pages visitors see (or will see): changes there get VisibleEdits' restore point (P8a statuses). */
private static function isLive(WP_Post $post): bool
{
return in_array((string) $post->post_status, GlobalUsages::LIVE, true);
}
/**
* The active global entry $libraryId (LibraryAccess::item(): the library right and edit_post on it) with
* its single readable root, holding no global module and naming no entry in any spelling (P11, Q7).
*
* @return array{0: int, 1: Block}|WP_Error
*/
private function globalRoot(mixed $libraryId): array|WP_Error
{
$id = PostGuard::id($libraryId);
if ($id === null) {
return Errors::invalidInput('library_id ist keine ID; nichts wurde gespeichert.', [
'library_id' => 'ID aus lit/library-list',
]);
}
$item = $this->access->item($id, false);
if ($item === null) {
return PostGuard::notFound($id);
}
if ($item instanceof WP_Error) {
return $item;
}
$content = (string) $item->post_content;
try {
$roots = LibraryContent::roots(LibraryContent::load($this->parser, $content));
} catch (MalformedMarkup) {
$roots = [];
}
$valid = (string) $item->post_status === 'publish' && $this->access->isGlobal($item) && count($roots) === 1
&& ! $roots[0]->isOpaque() && ! LibraryContent::containsGlobal($roots[0])
&& ! UsageMatcher::referencesAny($content);
if (! $valid) {
return Errors::invalidInput(
sprintf(
'Bibliothekseintrag %d ist kein aktiver globaler Eintrag mit genau einem Knoten ohne Verweis auf '
. 'andere Einträge; nichts wurde gespeichert.',
$id
),
['library_id' => 'globaler Eintrag (lit/library-list, scope "global")']
);
}
return [$id, $roots[0]];
}
/**
* Only pages and posts (POST_TYPES, ruling Q25): a library entry would nest global modules (P11), a Theme
* Builder layout is left to lit/library-save plus lit-library-insert (ruling Q24), other Divi-enabled types
* are out of scope.
*/
private static function sourceRefusal(DiviPage $page): ?WP_Error
{
if ($page->isLibraryItem()) {
return Errors::invalidInput(
'Knoten in Bibliothekseinträgen lassen sich nicht in globale Module umwandeln (keine '
. 'Verschachtelung); nichts wurde angelegt.',
['post_id' => 'eine Seite oder ein Beitrag']
);
}
if ($page->isThemeBuilderLayout()) {
return Errors::invalidInput(
'Knoten in Theme-Builder-Layouts wandelt lit/library-make-global nicht um; nichts wurde angelegt.',
['post_id' => 'eine Seite oder ein Beitrag – im Layout mit lit/library-save (scope "global") und '
. 'lit-library-insert (mode "global")']
);
}
if (! in_array((string) $page->post->post_type, self::POST_TYPES, true)) {
return Errors::invalidInput(
'lit/library-make-global arbeitet nur auf Seiten und Beiträgen; nichts wurde gespeichert.',
['post_id' => 'eine Seite oder ein Beitrag (Inhaltstyp page oder post)']
);
}
return null;
}
/**
* The page's Divi write result plus the new entry: lit_mcp_after_save and a change-log row for the entry
* (like lit/library-save), library_id and the hint on further occurrences.
*
* @param array<string, mixed> $saved
* @param list<string> $entryWarnings
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
private function result(
array $saved,
LibraryItemSpec $spec,
int $libraryId,
array $entryWarnings,
DiviPage $page,
array $input
): array {
$entry = (new PostSaveFollowUp($this->name(), $this->log, $libraryId, 'der Bibliothekseintrag', $entryWarnings))
->afterSave($input)
->log(DiviLibraryStore::POST_TYPE, sprintf(
'Bibliothek: „%s“ angelegt (%s, global) aus Eintrag %d und dort als Instanz eingesetzt',
$spec->title,
self::TYPE_LABELS[$spec->layoutType] ?? $spec->layoutType,
(int) $page->post->ID
));
// The entry's warnings, including those of its follow-up (listener, change log).
$warnings = $entry->result('', null)->toArray()['warnings'];
$saved['library_id'] = $libraryId;
$saved['warnings'] = array_merge(
is_array($saved['warnings'] ?? null) ? $saved['warnings'] : [],
$warnings,
[sprintf(
'Gleichartige Vorkommen: lit/library-find-similar mit library_id %d; inhaltsgleiche („identical“) '
. 'mit lit/library-make-global (library_id, targets) durch das globale Modul ersetzen.',
$libraryId
)]
);
return $saved;
}
}