feat: SnapshotStore – Snapshots des Designsystems je Bereich
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
388b52ea76
commit
5ba52b98b3
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Snapshots of the design system (spec §5, G6): one option per area (lit_mcp_snapshots_<area>, never autoloaded),
|
||||
* newest first, at most LIMIT entries – the oldest falls out. An entry keeps the area's raw inventory as Divi stores
|
||||
* it plus who made it with which tool and the change log entry. The only way back for colors, variables and presets:
|
||||
* Divi 5 keeps no server-side history (formats §7.6). Not final so abilities can be tested with a mock.
|
||||
*/
|
||||
class SnapshotStore
|
||||
{
|
||||
public const AREAS = ['colors', 'variables', 'presets', 'theme_options'];
|
||||
|
||||
public const LIMIT = 20;
|
||||
|
||||
/** Spec §5: a larger snapshot refuses the change. */
|
||||
public const MAX_BYTES = 2000000;
|
||||
|
||||
private const PREFIX = 'lit_mcp_snapshots_';
|
||||
|
||||
public static function option(string $area): string
|
||||
{
|
||||
if (! in_array($area, self::AREAS, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Unknown design area "%s".', $area));
|
||||
}
|
||||
|
||||
return self::PREFIX . $area;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $data The area's raw inventory.
|
||||
* @throws SnapshotTooLarge
|
||||
* @throws RuntimeException When the option could not be written.
|
||||
*/
|
||||
public function save(string $area, array $data, string $ability): string
|
||||
{
|
||||
$size = strlen((string) wp_json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
if ($size > self::MAX_BYTES) {
|
||||
throw new SnapshotTooLarge($size);
|
||||
}
|
||||
|
||||
$id = sprintf('%s-%s-%s', $area, gmdate('Ymd\THis\Z'), bin2hex(random_bytes(3)));
|
||||
$entries = $this->entries($area);
|
||||
array_unshift($entries, [
|
||||
'id' => $id,
|
||||
'created' => gmdate('c'),
|
||||
'user_id' => get_current_user_id(),
|
||||
'ability' => $ability,
|
||||
'log_id' => null,
|
||||
'size' => $size,
|
||||
'data' => $data,
|
||||
]);
|
||||
if (! update_option(self::option($area), array_slice($entries, 0, self::LIMIT), false)) {
|
||||
throw new RuntimeException(sprintf('snapshot %s could not be stored', $area));
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** Notes the change log entry of the change the snapshot belongs to (the log entry is written after it). */
|
||||
public function attachLog(string $area, string $id, ?int $logId): void
|
||||
{
|
||||
if ($logId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entries = $this->entries($area);
|
||||
foreach ($entries as $index => $entry) {
|
||||
if ($entry['id'] === $id) {
|
||||
$entries[$index]['log_id'] = $logId;
|
||||
update_option(self::option($area), $entries, false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<array{id: string, created: string, user_id: int, ability: string, log_id: ?int, size: int}> */
|
||||
public function list(string $area): array
|
||||
{
|
||||
return array_map(static function (array $entry): array {
|
||||
unset($entry['data']);
|
||||
|
||||
return $entry;
|
||||
}, $this->entries($area));
|
||||
}
|
||||
|
||||
/** @return array{id: string, created: string, user_id: int, ability: string, log_id: ?int, size: int,
|
||||
* data: array<array-key, mixed>}|null */
|
||||
public function get(string $area, string $id): ?array
|
||||
{
|
||||
foreach ($this->entries($area) as $entry) {
|
||||
if ($entry['id'] === $id) {
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return list<array{id: string, created: string, user_id: int, ability: string, log_id: ?int, size: int,
|
||||
* data: array<array-key, mixed>}> */
|
||||
private function entries(string $area): array
|
||||
{
|
||||
$stored = get_option(self::option($area), []);
|
||||
if (! is_array($stored)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ($stored as $entry) {
|
||||
if (is_array($entry) && is_string($entry['id'] ?? null) && is_array($entry['data'] ?? null)) {
|
||||
$entries[] = [
|
||||
'id' => $entry['id'],
|
||||
'created' => (string) ($entry['created'] ?? ''),
|
||||
'user_id' => (int) ($entry['user_id'] ?? 0),
|
||||
'ability' => (string) ($entry['ability'] ?? ''),
|
||||
'log_id' => isset($entry['log_id']) ? (int) $entry['log_id'] : null,
|
||||
'size' => (int) ($entry['size'] ?? 0),
|
||||
'data' => $entry['data'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/** A snapshot above SnapshotStore::MAX_BYTES (spec §5): the change is refused instead of writing without a backup. */
|
||||
final class SnapshotTooLarge extends RuntimeException
|
||||
{
|
||||
public function __construct(public readonly int $size)
|
||||
{
|
||||
parent::__construct(sprintf('snapshot of %d bytes exceeds %d', $size, SnapshotStore::MAX_BYTES));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user