diff --git a/src/Integrations/Divi/Library/DiviLibraryStore.php b/src/Integrations/Divi/Library/DiviLibraryStore.php index 7da3e2b..d39c65d 100644 --- a/src/Integrations/Divi/Library/DiviLibraryStore.php +++ b/src/Integrations/Divi/Library/DiviLibraryStore.php @@ -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 + */ + 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)); diff --git a/src/Integrations/Divi/Library/DiviRoles.php b/src/Integrations/Divi/Library/DiviRoles.php index a28c9f0..97090a0 100644 --- a/src/Integrations/Divi/Library/DiviRoles.php +++ b/src/Integrations/Divi/Library/DiviRoles.php @@ -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) { diff --git a/src/Integrations/Divi/Library/LibraryItemSpec.php b/src/Integrations/Divi/Library/LibraryItemSpec.php index e5fa4c8..4ea6edc 100644 --- a/src/Integrations/Divi/Library/LibraryItemSpec.php +++ b/src/Integrations/Divi/Library/LibraryItemSpec.php @@ -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 */ - 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; - } } diff --git a/src/Integrations/Divi/Library/NewLibraryEntry.php b/src/Integrations/Divi/Library/NewLibraryEntry.php index ee27b5d..70f04cd 100644 --- a/src/Integrations/Divi/Library/NewLibraryEntry.php +++ b/src/Integrations/Divi/Library/NewLibraryEntry.php @@ -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'; } diff --git a/src/Integrations/Divi/Release/DiviAdapters.php b/src/Integrations/Divi/Release/DiviAdapters.php index 69bd9c8..661cb54 100644 --- a/src/Integrations/Divi/Release/DiviAdapters.php +++ b/src/Integrations/Divi/Release/DiviAdapters.php @@ -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> */ public const CLASSES = [ DiviPages::class, + DiviLibraryStore::class, + DiviRoles::class, DiviModules::class, DynamicContentMap::class, ]; diff --git a/tests/Unit/Integrations/Divi/Library/DiviLibraryStoreTest.php b/tests/Unit/Integrations/Divi/Library/DiviLibraryStoreTest.php index a14d665..8490011 100644 --- a/tests/Unit/Integrations/Divi/Library/DiviLibraryStoreTest.php +++ b/tests/Unit/Integrations/Divi/Library/DiviLibraryStoreTest.php @@ -8,6 +8,8 @@ use Brain\Monkey\Functions; use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore; use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec; +use LIT\MCP\Integrations\Divi\Release\DiviAdapters; +use LIT\MCP\Integrations\Divi\Release\Manifest; use LIT\MCP\Integrations\WordPress\Content\NewDraft; use LIT\MCP\Tests\Support\FakeChangeLog; use LIT\MCP\Tests\Unit\TestCase; @@ -114,6 +116,32 @@ final class DiviLibraryStoreTest extends TestCase self::assertSame(12, $captured['author']); } + public function testIsTheRegisteredLibraryAdapterWithTheSubmitLayoutMirror(): void + { + $manifest = DiviLibraryStore::manifest(); + + self::assertContains(DiviLibraryStore::class, DiviAdapters::CLASSES); + self::assertSame(Manifest::LIBRARY, $manifest->area); + self::assertSame(['et_pb_layout'], $manifest->postTypes); + self::assertSame( + ['scope', 'layout_type', 'module_width', 'layout_category', 'layout_tag'], + $manifest->taxonomies + ); + self::assertSame(['library-submit-layout'], array_map(static fn ($m): string => $m->id, $manifest->mirrors)); + self::assertSame('et_pb_submit_layout()', $manifest->mirrors[0]->label()); + } + + public function testBuiltForReadsTheSourceType(): void + { + Functions\when('get_post_meta')->alias( + static fn (int $id, string $key): string => $id === 1054 + && $key === '_et_pb_built_for_post_type' ? 'post' : '' + ); + + self::assertSame('post', DiviLibraryStore::builtFor(1054)); + self::assertSame('', DiviLibraryStore::builtFor(1055)); + } + public function testReportsMissingTaxonomies(): void { Functions\when('taxonomy_exists')->alias(static fn (string $taxonomy): bool => $taxonomy !== 'scope'); diff --git a/tests/Unit/Integrations/Divi/Library/DiviRolesTest.php b/tests/Unit/Integrations/Divi/Library/DiviRolesTest.php index 876e5cf..330e879 100644 --- a/tests/Unit/Integrations/Divi/Library/DiviRolesTest.php +++ b/tests/Unit/Integrations/Divi/Library/DiviRolesTest.php @@ -26,4 +26,10 @@ final class DiviRolesTest extends TestCase // Unit tests have no et_pb_is_allowed(): a missing Divi function never means "allowed". self::assertFalse((new DiviRoles())->allows('divi_library')); } + + public function testIsTheRegisteredRolesAdapter(): void + { + self::assertContains(DiviRoles::class, \LIT\MCP\Integrations\Divi\Release\DiviAdapters::CLASSES); + self::assertSame('et_pb_is_allowed()', DiviRoles::manifest()->calls[0]->label()); + } } diff --git a/tests/Unit/Integrations/Divi/Library/LibraryItemSpecTest.php b/tests/Unit/Integrations/Divi/Library/LibraryItemSpecTest.php index 44d2b20..5bee53a 100644 --- a/tests/Unit/Integrations/Divi/Library/LibraryItemSpecTest.php +++ b/tests/Unit/Integrations/Divi/Library/LibraryItemSpecTest.php @@ -8,6 +8,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Block; use LIT\MCP\Integrations\Divi\BlockTree\LibraryContent; use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex; use LIT\MCP\Integrations\Divi\BlockTree\Parser; +use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore; use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec; use LIT\MCP\Tests\Support\DiviFixtures; use PHPUnit\Framework\TestCase; @@ -60,7 +61,7 @@ final class LibraryItemSpecTest extends TestCase self::assertEquals($expectedTerms, $spec->terms()); $expectedMeta = array_map(static fn (array $values): string => $values[0], $json['meta']); unset($expectedMeta['_et_pb_row_layout']); - self::assertEquals($expectedMeta, $spec->meta()); + self::assertEquals($expectedMeta, DiviLibraryStore::metaFor($spec)); } public function testRefusesInstancesColumnsAndGlobalsInsideGlobals(): void diff --git a/tests/Unit/Integrations/Divi/Library/LibrarySaveAbilityTest.php b/tests/Unit/Integrations/Divi/Library/LibrarySaveAbilityTest.php index 65983b8..5fca4c3 100644 --- a/tests/Unit/Integrations/Divi/Library/LibrarySaveAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Library/LibrarySaveAbilityTest.php @@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Library; use Brain\Monkey\Functions; use LIT\MCP\Abilities\Rev; use LIT\MCP\Abilities\WriteFailed; +use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore; use LIT\MCP\Integrations\Divi\Library\LibraryItemSpec; use LIT\MCP\Integrations\Divi\Library\LibrarySaveAbility; use LIT\MCP\Integrations\Divi\Pages\DiviSaver; @@ -112,8 +113,8 @@ final class LibrarySaveAbilityTest extends TestCase self::assertTrue($result['ok']); self::assertTrue($this->specs[0]->global); - self::assertSame('divi/button', $this->specs[0]->meta()['_et_pb_module_type']); - self::assertSame('[]', $this->specs[0]->meta()['_et_pb_excluded_global_options']); + self::assertSame('divi/button', DiviLibraryStore::metaFor($this->specs[0])['_et_pb_module_type']); + self::assertSame('[]', DiviLibraryStore::metaFor($this->specs[0])['_et_pb_excluded_global_options']); } public function testRefusesAGlobalInstanceAndColumnsAndCreatesNothing(): void diff --git a/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php b/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php index 20b6b92..5fb5a40 100644 --- a/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php +++ b/tests/Unit/Integrations/Divi/Release/DiviBoundaryTest.php @@ -26,10 +26,6 @@ final class DiviBoundaryTest extends TestCase private const LEGACY = [ 'Integrations/Divi/Design/DesignSystemReader.php', 'Integrations/Divi/DiviCompatibility.php', - 'Integrations/Divi/Library/DiviLibraryStore.php', - 'Integrations/Divi/Library/DiviRoles.php', - 'Integrations/Divi/Library/LibraryItemSpec.php', - 'Integrations/Divi/Library/NewLibraryEntry.php', 'Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php', 'Integrations/Media/MediaUsageFinder.php', ];