diff --git a/src/Integrations/Divi/DiviCompatibility.php b/src/Integrations/Divi/DiviCompatibility.php index 7e57b19..ec24dd0 100644 --- a/src/Integrations/Divi/DiviCompatibility.php +++ b/src/Integrations/Divi/DiviCompatibility.php @@ -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; - } } diff --git a/src/Integrations/Divi/Pages/DynamicContentMap.php b/src/Integrations/Divi/Pages/DynamicContentMap.php index e2457e8..704f291 100644 --- a/src/Integrations/Divi/Pages/DynamicContentMap.php +++ b/src/Integrations/Divi/Pages/DynamicContentMap.php @@ -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>|null Null = use DynamicContentRules' default map. */ public function forPost(int $postId): ?array { diff --git a/src/Integrations/Divi/Release/DiviAdapters.php b/src/Integrations/Divi/Release/DiviAdapters.php index 5baf27d..86b9c9d 100644 --- a/src/Integrations/Divi/Release/DiviAdapters.php +++ b/src/Integrations/Divi/Release/DiviAdapters.php @@ -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> */ - public const CLASSES = []; + public const CLASSES = [ + DiviModules::class, + DynamicContentMap::class, + ]; /** @return array, Manifest> */ public static function manifests(): array diff --git a/src/Integrations/Divi/Schema/DiviModules.php b/src/Integrations/Divi/Schema/DiviModules.php index a35f352..30cafb0 100644 --- a/src/Integrations/Divi/Schema/DiviModules.php +++ b/src/Integrations/Divi/Schema/DiviModules.php @@ -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; + } } diff --git a/src/Integrations/WordPress/Schema/ContentSchema.php b/src/Integrations/WordPress/Schema/ContentSchema.php index 7a4621f..d9a03a9 100644 --- a/src/Integrations/WordPress/Schema/ContentSchema.php +++ b/src/Integrations/WordPress/Schema/ContentSchema.php @@ -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 */ diff --git a/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php b/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php index a772d27..0b663a9 100644 --- a/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php +++ b/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php @@ -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 diff --git a/tests/Unit/Integrations/Divi/Schema/DiviModulesTest.php b/tests/Unit/Integrations/Divi/Schema/DiviModulesTest.php new file mode 100644 index 0000000..0fd1d5b --- /dev/null +++ b/tests/Unit/Integrations/Divi/Schema/DiviModulesTest.php @@ -0,0 +1,59 @@ +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')); + } +} diff --git a/tests/Unit/Integrations/WordPress/Schema/ContentSchemaTest.php b/tests/Unit/Integrations/WordPress/Schema/ContentSchemaTest.php index 8a780c2..705d8f9 100644 --- a/tests/Unit/Integrations/WordPress/Schema/ContentSchemaTest.php +++ b/tests/Unit/Integrations/WordPress/Schema/ContentSchemaTest.php @@ -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 + ); + } }