refactor: Divi catalog adapter with manifest

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 00:54:48 +02:00
co-authored by Claude Opus 5.5
parent 5e6d5ff3e1
commit fc7a84d239
8 changed files with 164 additions and 46 deletions
+1 -29
View File
@@ -60,7 +60,7 @@ final class DiviCompatibility
static fn (): bool => method_exists(DiviLibraryStore::GLOBAL_LAYOUT, 'is_global_layout_template'),
'ET_Core_PageResource (CSS-Cache)' => static fn (): bool => class_exists('ET_Core_PageResource'),
'Block-Typ divi/global-layout (Instanzen globaler Module)' =>
static fn (): bool => self::knowsGlobalLayout(),
static fn (): bool => DiviModules::knowsBlock(GlobalInstance::BLOCK),
'Taxonomien layout_category, layout_tag (Bibliothek)' =>
static fn (): bool => taxonomy_exists('layout_category') && taxonomy_exists('layout_tag'),
// Includes et_theme_builder_clear_wp_cache (Theme Builder cache of every site-wide clear, review M-2).
@@ -72,32 +72,4 @@ final class DiviCompatibility
) === [],
];
}
/**
* Whether Divi still has the block type of global instances (GlobalInstance::BLOCK): registered
* with WordPress, or in Divi's core module metadata (Divi renders instances itself, BlockParser,
* and does not always register the type with WordPress).
*/
private static function knowsGlobalLayout(): bool
{
if (
class_exists('WP_Block_Type_Registry')
&& \WP_Block_Type_Registry::get_instance()->is_registered(GlobalInstance::BLOCK)
) {
return true;
}
$registration = 'ET\Builder\Packages\ModuleLibrary\ModuleRegistration';
if (! method_exists($registration, 'get_all_core_modules_metadata')) {
return false;
}
foreach ((array) $registration::get_all_core_modules_metadata() as $module) {
if (is_array($module) && ($module['name'] ?? null) === GlobalInstance::BLOCK) {
return true;
}
}
return false;
}
}
@@ -7,13 +7,16 @@ namespace LIT\MCP\Integrations\Divi\Pages;
use Closure;
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Log;
use Throwable;
defined('ABSPATH') || exit;
/**
* DynamicContentRules' dynamic content allow map (ruling S22) from Divi's live registry
* Adapter "Katalog" (spec §3): DynamicContentRules' dynamic content allow map (ruling S22) from Divi's live registry
* DynamicContentOptions::get_options($postId, 'edit') – the options the Visual Builder offers
* for this entry (filter divi_module_dynamic_content_options; the list depends on the post:
* post_categories/post_tags only for types with those taxonomies). Shape:
@@ -29,9 +32,9 @@ defined('ABSPATH') || exit;
*
* Not final so abilities can be tested with a fake registry.
*/
class DynamicContentMap
class DynamicContentMap implements DiviAdapter
{
/** Divi's dynamic-content registry class; also checked by DiviCompatibility. */
/** Divi's dynamic-content registry class; listed in manifest(). */
public const REGISTRY = 'ET\Builder\Packages\Module\Layout\Components\DynamicContent\DynamicContentOptions';
/**
@@ -41,6 +44,11 @@ class DynamicContentMap
{
}
public static function manifest(): Manifest
{
return new Manifest(Manifest::CATALOG, calls: [new Call(self::REGISTRY . '::get_options', 2)]);
}
/** @return array<string, array<string, string>>|null Null = use DynamicContentRules' default map. */
public function forPost(int $postId): ?array
{
@@ -4,13 +4,19 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
defined('ABSPATH') || exit;
/** The Divi boundary (spec §3): the only classes that touch Divi, in the order reports list them. */
final class DiviAdapters
{
/** @var list<class-string<DiviAdapter>> */
public const CLASSES = [];
public const CLASSES = [
DiviModules::class,
DynamicContentMap::class,
];
/** @return array<class-string<DiviAdapter>, Manifest> */
public static function manifests(): array
+68 -3
View File
@@ -5,17 +5,51 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Schema;
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;
use LIT\MCP\Integrations\Divi\BlockTree\GlobalInstance;
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
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\ThemeBuilder\DiviThemeBuilder;
defined('ABSPATH') || exit;
/**
* Reads Divi's module metadata once per request (Divi 5 only). Not final so abilities can be
* unit-tested with a mock.
* Adapter "Katalog" (spec §3): Divi's module metadata once per request (Divi 5 only), Divi's own post types and
* its per-type builder switch for the content tools, and whether Divi knows a block type. Not final so abilities
* can be unit-tested with a mock.
*/
class DiviModules
class DiviModules implements DiviAdapter
{
/** Divi's own post types the content tools leave alone (library, Theme Builder, canvases, speculation). */
public const INTERNAL_POST_TYPES = [
DiviLibraryStore::POST_TYPE,
DiviThemeBuilder::THEME_BUILDER,
DiviThemeBuilder::TEMPLATE,
DiviThemeBuilder::LAYOUT_TYPES['header'],
DiviThemeBuilder::LAYOUT_TYPES['body'],
DiviThemeBuilder::LAYOUT_TYPES['footer'],
'et_pb_canvas',
'_et_pb_speculation',
];
private ?ModuleCatalog $catalog = null;
public static function manifest(): Manifest
{
return new Manifest(
Manifest::CATALOG,
calls: [
new Call(ModuleRegistration::class . '::get_all_core_modules_metadata', 0),
new Call(ModuleRegistration::class . '::get_default_attrs', 2),
new Call('et_builder_enabled_for_post_type', 1),
],
constants: ['ET_BUILDER_PRODUCT_VERSION'],
blockTypes: [GlobalInstance::BLOCK],
keys: ['et_pb_canvas', '_et_pb_speculation']
);
}
public static function isAvailable(): bool
{
return class_exists(ModuleRegistration::class);
@@ -43,4 +77,35 @@ class DiviModules
{
return defined('ET_BUILDER_PRODUCT_VERSION') ? (string) ET_BUILDER_PRODUCT_VERSION : '5.2.1';
}
/**
* Whether Divi's builder is switched on for $postType (Divi → Theme-Optionen → Builder); false without Divi.
*/
public static function builderEnabledFor(string $postType): bool
{
return function_exists('et_builder_enabled_for_post_type')
&& (bool) et_builder_enabled_for_post_type($postType);
}
/**
* Whether Divi still has block type $name: registered with WordPress, or in Divi's core module metadata (Divi
* renders global instances itself and does not always register the type with WordPress).
*/
public static function knowsBlock(string $name): bool
{
if (class_exists('WP_Block_Type_Registry') && \WP_Block_Type_Registry::get_instance()->is_registered($name)) {
return true;
}
if (! self::isAvailable()) {
return false;
}
foreach ((array) ModuleRegistration::get_all_core_modules_metadata() as $module) {
if (is_array($module) && ($module['name'] ?? null) === $name) {
return true;
}
}
return false;
}
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Schema;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LogicException;
defined('ABSPATH') || exit;
@@ -18,9 +19,8 @@ class ContentSchema
/** Post types handled elsewhere (media tools, Divi tools, WordPress internals). */
public const EXCLUDED = [
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout',
'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout',
'et_pb_canvas', '_et_pb_speculation',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
...DiviModules::INTERNAL_POST_TYPES,
];
private const COUNTED_STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
@@ -36,10 +36,7 @@ class ContentSchema
*/
public function __construct(private array $sources, ?callable $diviEnabled = null)
{
$this->diviEnabled = $diviEnabled ?? static function (string $postType): bool {
return function_exists('et_builder_enabled_for_post_type')
&& (bool) et_builder_enabled_for_post_type($postType);
};
$this->diviEnabled = $diviEnabled ?? DiviModules::builderEnabledFor(...);
}
/** @return list<string> */
@@ -33,13 +33,10 @@ final class DiviBoundaryTest extends TestCase
'Integrations/Divi/Pages/DiviFlags.php',
'Integrations/Divi/Pages/DiviPage.php',
'Integrations/Divi/Pages/DiviSaver.php',
'Integrations/Divi/Pages/DynamicContentMap.php',
'Integrations/Divi/Pages/WpLinkTargets.php',
'Integrations/Divi/Schema/DiviModules.php',
'Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php',
'Integrations/Media/MediaUsageFinder.php',
'Integrations/WordPress/Content/PostPresenter.php',
'Integrations/WordPress/Schema/ContentSchema.php',
];
public function testOnlyAdaptersTouchDivi(): void
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Schema;
use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
use LIT\MCP\Integrations\Divi\Release\Call;
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
use LIT\MCP\Integrations\Divi\Release\Manifest;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LIT\MCP\Tests\Unit\TestCase;
final class DiviModulesTest extends TestCase
{
public function testCatalogAdaptersAreRegistered(): void
{
self::assertContains(DiviModules::class, DiviAdapters::CLASSES);
self::assertContains(DynamicContentMap::class, DiviAdapters::CLASSES);
self::assertSame(Manifest::CATALOG, DiviModules::manifest()->area);
self::assertSame(Manifest::CATALOG, DynamicContentMap::manifest()->area);
}
public function testManifestListsModuleRegistrationAndTheBuilderSwitch(): void
{
$manifest = DiviModules::manifest();
self::assertSame(
[
'ModuleRegistration::get_all_core_modules_metadata()',
'ModuleRegistration::get_default_attrs()',
'et_builder_enabled_for_post_type()',
],
array_map(static fn (Call $call): string => $call->label(), $manifest->calls)
);
self::assertSame(['divi/global-layout'], $manifest->blockTypes);
self::assertSame(['ET_BUILDER_PRODUCT_VERSION'], $manifest->constants);
}
public function testInternalPostTypesAreDivisOwn(): void
{
self::assertSame(
[
'et_pb_layout', 'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout',
'et_footer_layout', 'et_pb_canvas', '_et_pb_speculation',
],
DiviModules::INTERNAL_POST_TYPES
);
}
public function testBuilderEnabledForAsksDivi(): void
{
Functions\when('et_builder_enabled_for_post_type')->alias(static fn (string $type): bool => $type === 'page');
self::assertTrue(DiviModules::builderEnabledFor('page'));
self::assertFalse(DiviModules::builderEnabledFor('post'));
}
}
@@ -117,4 +117,18 @@ final class ContentSchemaTest extends TestCase
self::assertSame(['logo', 'photo'], (new ContentSchema([$source]))->mediaFieldKeys());
}
/** Spec §3: Divi's post types come from the Katalog adapter – the list itself stays the same. */
public function testExcludedPostTypesStayTheSame(): void
{
self::assertSame(
[
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face', 'et_pb_layout',
'et_theme_builder', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout',
'et_pb_canvas', '_et_pb_speculation',
],
ContentSchema::EXCLUDED
);
}
}