Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
79 lines
2.7 KiB
PHP
79 lines
2.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Integrations\Divi\BlockTree;
|
||
|
||
use JsonException;
|
||
use stdClass;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* JSON of block attributes. PHP arrays cannot tell an empty JSON object from an empty list,
|
||
* nor an object with the keys "0", "1", … from a list. decodeObject() therefore keeps such
|
||
* objects as stdClass, so a regenerated opener encodes them as {} again (Divi breaks on
|
||
* "[]" where it expects "{}" – see its EmptyArrayCorruptionMigration).
|
||
*/
|
||
final class Json
|
||
{
|
||
/**
|
||
* @return array<string, mixed>|null Null when $json is not a JSON object, or its top level
|
||
* is one only nominally (keys "0", "1", … – see the class docblock): re-encoding those
|
||
* keys as a PHP array would silently turn the object back into a JSON list.
|
||
*/
|
||
public static function decodeObject(string $json): ?array
|
||
{
|
||
try {
|
||
$value = json_decode($json, false, 512, JSON_THROW_ON_ERROR);
|
||
} catch (JsonException) {
|
||
return null;
|
||
}
|
||
|
||
if (! $value instanceof stdClass) {
|
||
return null;
|
||
}
|
||
|
||
$keys = get_object_vars($value);
|
||
if ($keys !== [] && array_is_list($keys)) {
|
||
return null;
|
||
}
|
||
|
||
$converted = self::fromJson($value);
|
||
|
||
return is_array($converted) ? $converted : [];
|
||
}
|
||
|
||
/** Converts json_decode(…, false) output: objects become arrays unless that would lose their object-ness. */
|
||
public static function fromJson(mixed $value): mixed
|
||
{
|
||
if ($value instanceof stdClass) {
|
||
$array = [];
|
||
foreach (get_object_vars($value) as $key => $item) {
|
||
$array[$key] = self::fromJson($item);
|
||
}
|
||
|
||
return $array === [] || array_is_list($array) ? (object) $array : $array;
|
||
}
|
||
|
||
if (is_array($value)) {
|
||
return array_map([self::class, 'fromJson'], $value);
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* @throws JsonException When $value cannot be represented as JSON – including a string
|
||
* with an invalid UTF-8 byte sequence (JSON_ERROR_UTF8). 2a only ever encodes
|
||
* attributes this plugin already decoded from valid block markup, so that never
|
||
* happens here; a caller that later encodes user-supplied strings (2b's writers)
|
||
* must catch this and map it to a validation error instead of a fatal.
|
||
*/
|
||
public static function encode(mixed $value): string
|
||
{
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- pure PHP (spec §4.4).
|
||
return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||
}
|
||
}
|