feat: Divi-Fähigkeiten (DiviFeature, Requirement, ConstantRequirement) an Call, Mirror und Manifest; alle acht Adapter getaggt; Tests für Vollständigkeit und die gepinnte Zuordnung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-18 21:59:43 +02:00
co-authored by Claude Sonnet 5.5
parent e6e0f914ba
commit 392fe7c569
19 changed files with 552 additions and 48 deletions
+15 -4
View File
@@ -10,11 +10,17 @@ defined('ABSPATH') || exit;
* One Divi function or static method an adapter calls (spec §4.1), with the number of arguments the plugin passes.
* $args null: only its existence matters (a function Divi itself needs, e.g. et_() inside store_template).
*/
final class Call
final class Call implements Requirement
{
/** @param string $symbol "function_name" or "Fully\Qualified\Class::method". */
public function __construct(public readonly string $symbol, public readonly ?int $args = null)
{
/**
* @param string $symbol "function_name" or "Fully\Qualified\Class::method".
* @param list<DiviFeature> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $symbol,
public readonly ?int $args = null,
private readonly array $features = []
) {
}
public function className(): ?string
@@ -27,6 +33,11 @@ final class Call
return str_contains($this->symbol, '::') ? explode('::', $this->symbol, 2)[1] : $this->symbol;
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->className();
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/** A Divi constant an adapter reads (spec §4.1); it must be defined. */
final class ConstantRequirement implements Requirement
{
/** @param list<DiviFeature> $features [] = the default of the manifest. */
public function __construct(public readonly string $name, private readonly array $features = [])
{
}
public function label(): string
{
return 'Konstante ' . $this->name;
}
public function features(): array
{
return $this->features;
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
/**
* What the plugin can do with Divi, as far as a Divi update may break it separately: every manifest entry (call,
* mirror, constant) belongs to one or more features, and the release gate decides per feature.
*/
enum DiviFeature: string
{
case PageRead = 'page-read';
case PageWrite = 'page-write';
case Catalog = 'catalog';
case DynamicSources = 'dynamic-sources';
case DesignRead = 'design-read';
case PresetWrite = 'preset-write';
case LibraryRead = 'library-read';
case LibraryWrite = 'library-write';
case Roles = 'roles';
case ThemeBuilderRead = 'tb-read';
case ThemeBuilderWrite = 'tb-write';
/** Whether the feature changes data (the four *Write cases). */
public function isWrite(): bool
{
return match ($this) {
self::PageWrite, self::PresetWrite, self::LibraryWrite, self::ThemeBuilderWrite => true,
default => false,
};
}
/** Name for messages, the status tool and the admin tab (German). */
public function label(): string
{
return match ($this) {
self::PageRead => 'Seiten lesen',
self::PageWrite => 'Seiten schreiben',
self::Catalog => 'Modulkatalog',
self::DynamicSources => 'Dynamische Inhalte',
self::DesignRead => 'Designsystem lesen',
self::PresetWrite => 'Presets schreiben',
self::LibraryRead => 'Bibliothek lesen',
self::LibraryWrite => 'Bibliothek schreiben',
self::Roles => 'Divi-Rollen',
self::ThemeBuilderRead => 'Theme Builder lesen',
self::ThemeBuilderWrite => 'Theme Builder schreiben',
};
}
}
+42 -12
View File
@@ -18,27 +18,56 @@ final class Manifest
public const DESIGN = 'Designsystem';
public const CATALOG = 'Katalog';
/** @var list<ConstantRequirement> */
public readonly array $constants;
/**
* @param string $area One of the constants above (the five areas of spec §3).
* @param list<Call> $calls Divi functions and static methods the adapter calls.
* @param list<string> $constants Divi constants it reads.
* @param list<string> $postTypes Divi post types it needs registered.
* @param list<string> $taxonomies Divi taxonomies it needs registered.
* @param list<string> $blockTypes Divi block types it needs (registered or in Divi's module metadata).
* @param list<string> $keys Other Divi names it uses as strings: meta keys, options, internal post
* types that need no registration check, query arguments.
* @param list<Mirror> $mirrors Divi functions whose storage logic it repeats.
* @param string $area One of the constants above (the five areas of spec §3).
* @param list<Call> $calls Divi functions and static methods the adapter calls.
* @param list<string|ConstantRequirement> $constants Divi constants it reads; a plain name stands for a
* ConstantRequirement without features of its own.
* @param list<string> $postTypes Divi post types it needs registered.
* @param list<string> $taxonomies Divi taxonomies it needs registered.
* @param list<string> $blockTypes Divi block types it needs (registered or in Divi's
* module metadata).
* @param list<string> $keys Other Divi names it uses as strings: meta keys, options,
* internal post types that need no registration check,
* query arguments.
* @param list<Mirror> $mirrors Divi functions whose storage logic it repeats.
* @param list<DiviFeature> $features The features of every call, mirror and constant that names
* none itself (featuresOf()).
*/
public function __construct(
public readonly string $area,
public readonly array $calls = [],
public readonly array $constants = [],
array $constants = [],
public readonly array $postTypes = [],
public readonly array $taxonomies = [],
public readonly array $blockTypes = [],
public readonly array $keys = [],
public readonly array $mirrors = []
public readonly array $mirrors = [],
public readonly array $features = []
) {
$this->constants = array_map(
static fn (string|ConstantRequirement $constant): ConstantRequirement => is_string($constant)
? new ConstantRequirement($constant)
: $constant,
array_values($constants)
);
}
/** @return list<Requirement> Calls, constants and mirrors – everything the release gate checks as code. */
public function requirements(): array
{
return [...$this->calls, ...$this->constants, ...$this->mirrors];
}
/** @return list<DiviFeature> The features $requirement names itself, else this manifest's default. */
public function featuresOf(Requirement $requirement): array
{
$own = $requirement->features();
return $own !== [] ? $own : $this->features;
}
/**
@@ -51,11 +80,12 @@ final class Manifest
foreach ($this->calls as $call) {
$names[] = $call->className() ?? $call->member();
}
$constants = array_map(static fn (ConstantRequirement $constant): string => $constant->name, $this->constants);
$mirrored = array_map(static fn (Mirror $mirror): string => $mirror->function, $this->mirrors);
return array_values(array_unique(array_merge(
$names,
$this->constants,
$constants,
$this->postTypes,
$this->taxonomies,
$this->keys,
@@ -57,7 +57,8 @@ class ManifestCheck
$checks[$manifest->area . ': ' . $call->label()] = static fn (): bool => self::accepts($call);
}
foreach ($manifest->constants as $constant) {
$checks[$manifest->area . ': Konstante ' . $constant] = static fn (): bool => defined($constant);
$checks[$manifest->area . ': ' . $constant->label()] =
static fn (): bool => defined($constant->name);
}
}
+16 -8
View File
@@ -10,25 +10,33 @@ defined('ABSPATH') || exit;
* A Divi function whose storage logic an adapter repeats instead of calling it (spec §4.1 "mirrors"). Its
* fingerprint (SourceFingerprint) must match the one in fingerprints.json, else Divi changed what we copy.
*/
final class Mirror
final class Mirror implements Requirement
{
/**
* @param string $id Key in fingerprints.json and name of tests/Fixtures/divi/reference/<id>.txt.
* @param string $file Divi file, relative to the Divi theme directory.
* @param string $function Function or method name.
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
* lives, and a short name is no Divi name for DiviBoundaryTest); null for a function.
* @param string $what What the plugin repeats (German, for reports).
* @param string $id Key in fingerprints.json and name of tests/Fixtures/divi/reference/<id>.txt.
* @param string $file Divi file, relative to the Divi theme directory.
* @param string $function Function or method name.
* @param string|null $class Class of a method – the short name is enough (the file fixes where it
* lives, and a short name is no Divi name for DiviBoundaryTest); null for a
* function.
* @param string $what What the plugin repeats (German, for reports).
* @param list<DiviFeature> $features [] = the default of the manifest.
*/
public function __construct(
public readonly string $id,
public readonly string $file,
public readonly string $function,
public readonly ?string $class,
public readonly string $what
public readonly string $what,
private readonly array $features = []
) {
}
public function features(): array
{
return $this->features;
}
public function label(): string
{
$class = $this->class === null ? '' : SourceFingerprint::shortName($this->class) . '::';
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
defined('ABSPATH') || exit;
/** One thing an adapter needs from Divi (a call, a mirrored function or a constant) and the features it serves. */
interface Requirement
{
/** Name for reports, e.g. "GlobalPreset::get_data()" (without the manifest area). */
public function label(): string;
/**
* The features this requirement serves itself; [] = the default of the manifest that lists it
* (Manifest::featuresOf() resolves that).
*
* @return list<DiviFeature>
*/
public function features(): array;
}