refactor: Divi library adapters with manifest and submit-layout mirror

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 01:16:23 +02:00
co-authored by Claude Opus 5.5
parent fed77b0584
commit e972898f52
10 changed files with 125 additions and 38 deletions
@@ -5,6 +5,10 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Library;
use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Release\Mirror;
use LIT\MCP\Integrations\WordPress\Content\NewDraft;
use RuntimeException;
use Throwable;
@@ -15,15 +19,15 @@ use WP_Term;
defined('ABSPATH') || exit;
/**
* Adapter for the Divi Library (E4, E15): post type et_pb_layout, the taxonomies scope,
* layout_type, module_width, layout_category, layout_tag and the meta Divi's et_pb_submit_layout()
* writes (F1, F2, fixtures stage3). "Global" is Divi's own definition
* Adapter "Bibliothek" (spec §3) for the Divi Library (E4, E15): post type et_pb_layout, the taxonomies scope,
* layout_type, module_width, layout_category, layout_tag and the terms and meta Divi's et_pb_submit_layout() writes
* (mirror library-submit-layout: metaFor()) (F1, F2, fixtures stage3). "Global" is Divi's own definition
* GlobalLayout::is_global_layout_template() (F5) with the scope term as fallback when the class is
* gone. find() filters by the scope term only, so an entry Divi counts as global through its D4
* meta fallback can show up under scope "normal" (preflight M-12, accepted). Library entries are
* always "publish" (F2). Not final so abilities can be tested with a mock.
*/
class DiviLibraryStore
class DiviLibraryStore implements DiviAdapter
{
public const POST_TYPE = 'et_pb_layout';
@@ -34,9 +38,67 @@ class DiviLibraryStore
/** Taxonomies every new entry needs (et_pb_submit_layout). */
public const TAXONOMIES = ['scope', 'layout_type', 'module_width'];
public const MODULE_TYPE = '_et_pb_module_type';
public const EXCLUDED_OPTIONS = '_et_pb_excluded_global_options';
public const BUILT_FOR = '_et_pb_built_for_post_type';
public const TEMPLATE_TYPE = '_et_pb_template_type';
/** Client text when the terms or meta of a new entry could not be written. */
public const REASON_CREATE = 'Der Bibliothekseintrag konnte nicht vollständig angelegt werden.';
public static function manifest(): Manifest
{
return new Manifest(
Manifest::LIBRARY,
calls: [new Call(self::GLOBAL_LAYOUT . '::is_global_layout_template', 1)],
postTypes: [self::POST_TYPE],
taxonomies: [...self::TAXONOMIES, 'layout_category', 'layout_tag'],
keys: [self::MODULE_TYPE, self::EXCLUDED_OPTIONS, self::BUILT_FOR, self::TEMPLATE_TYPE],
mirrors: [
new Mirror(
'library-submit-layout',
'includes/builder/shortcode-core.php',
'et_pb_submit_layout',
null,
'Terme und Meta neuer Bibliothekseinträge (scope, layout_type, module_width, _et_pb_module_type, '
. '_et_pb_excluded_global_options, _et_pb_built_for_post_type, _et_pb_template_type)'
),
]
);
}
/**
* The meta of a new entry as et_pb_submit_layout() writes it (compared with the real builder entries, fixtures
* stage3): _et_pb_module_type (modules), _et_pb_excluded_global_options = "[]" (global modules),
* _et_pb_built_for_post_type, _et_pb_template_type. _et_pb_row_layout is left out – Divi 5.2.1 stores
* "[object Object]" there (finding 5).
*
* @return array<string, string>
*/
public static function metaFor(LibraryItemSpec $spec): array
{
$meta = [];
if ($spec->moduleType !== null) {
$meta[self::MODULE_TYPE] = $spec->moduleType;
}
if ($spec->global && $spec->layoutType === 'module') {
$meta[self::EXCLUDED_OPTIONS] = '[]';
}
$meta[self::BUILT_FOR] = $spec->builtFor;
$meta[self::TEMPLATE_TYPE] = $spec->layoutType;
return $meta;
}
/** _et_pb_built_for_post_type of entry $id ('' when unset): the type an entry was built for. */
public static function builtFor(int $id): string
{
return (string) get_post_meta($id, self::BUILT_FOR, true);
}
public function isGlobal(int $id): bool
{
if (method_exists(self::GLOBAL_LAYOUT, 'is_global_layout_template')) {
@@ -127,7 +189,7 @@ class DiviLibraryStore
public function describe(WP_Post $post): array
{
$id = (int) $post->ID;
$moduleType = (string) get_post_meta($id, '_et_pb_module_type', true);
$moduleType = (string) get_post_meta($id, self::MODULE_TYPE, true);
return [
'id' => $id,
@@ -209,7 +271,7 @@ class DiviLibraryStore
throw new RuntimeException(sprintf('terms %s: %s', $taxonomy, $result->get_error_message()));
}
}
foreach ($spec->meta() as $key => $value) {
foreach (self::metaFor($spec) as $key => $value) {
update_post_meta($id, $key, $value);
if ((string) get_post_meta($id, $key, true) !== $value) {
throw new RuntimeException(sprintf('meta %s of library entry %d not stored', $key, $id));
+10 -2
View File
@@ -6,18 +6,21 @@ namespace LIT\MCP\Integrations\Divi\Library;
use Closure;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use WP_Error;
defined('ABSPATH') || exit;
/**
* Adapter for Divi's role editor (E3, E15): et_pb_is_allowed() (Divi 5.2.1 builder/core.php:1128)
* Adapter "Bibliothek" (spec §3) for Divi's role editor (E3, E15): et_pb_is_allowed() (Divi's builder/core.php)
* answers for the current user's role, "dangerous" options (theme_builder,
* read_dynamic_content_custom_fields) default to off for every role but administrator. When
* Divi no longer has the function, nothing is allowed – DiviCompatibility reports it.
* Not final so tests can pass a check.
*/
class DiviRoles
class DiviRoles implements DiviAdapter
{
private const LABELS = [
'divi_library' => 'Divi-Bibliothek',
@@ -28,6 +31,11 @@ class DiviRoles
'theme_builder' => 'Theme Builder',
];
public static function manifest(): Manifest
{
return new Manifest(Manifest::LIBRARY, calls: [new Call('et_pb_is_allowed', 1)]);
}
/** @param (Closure(string): bool)|null $check Test seam; null = Divi's et_pb_is_allowed(). */
public function __construct(private ?Closure $check = null)
{
@@ -13,12 +13,9 @@ use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
defined('ABSPATH') || exit;
/**
* Title, terms and meta of a new library entry, like Divi's et_pb_submit_layout() writes them
* (builder/shortcode-core.php:654-734; compared with the real builder entries, fixtures stage3):
* terms scope (global / non_global), layout_type, module_width, optional layout_category/layout_tag;
* meta _et_pb_module_type (modules), _et_pb_excluded_global_options = "[]" (global modules),
* _et_pb_built_for_post_type, _et_pb_template_type. _et_pb_row_layout is left out – Divi 5.2.1 stores
* "[object Object]" there (finding 5). Pure PHP.
* Title and terms of a new library entry, like Divi's et_pb_submit_layout() writes them (compared with the real
* builder entries, fixtures stage3): terms scope (global / non_global), layout_type, module_width, optional
* layout_category/layout_tag. The meta follows from DiviLibraryStore::metaFor(). Pure PHP.
*/
final class LibraryItemSpec
{
@@ -129,20 +126,4 @@ final class LibraryItemSpec
return $terms;
}
/** @return array<string, string> */
public function meta(): array
{
$meta = [];
if ($this->moduleType !== null) {
$meta['_et_pb_module_type'] = $this->moduleType;
}
if ($this->global && $this->layoutType === 'module') {
$meta['_et_pb_excluded_global_options'] = '[]';
}
$meta['_et_pb_built_for_post_type'] = $this->builtFor;
$meta['_et_pb_template_type'] = $this->layoutType;
return $meta;
}
}
@@ -127,7 +127,7 @@ final class NewLibraryEntry
return (string) $page->post->post_type;
}
$builtFor = (string) get_post_meta((int) $page->post->ID, '_et_pb_built_for_post_type', true);
$builtFor = DiviLibraryStore::builtFor((int) $page->post->ID);
return $builtFor !== '' ? $builtFor : 'page';
}
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
@@ -16,6 +18,8 @@ final class DiviAdapters
/** @var list<class-string<DiviAdapter>> */
public const CLASSES = [
DiviPages::class,
DiviLibraryStore::class,
DiviRoles::class,
DiviModules::class,
DynamicContentMap::class,
];