refactor: Divi catalog adapter with manifest
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user