One stored variable (formats §2). */ public static function variable( string $type, string $id, string $label, string $value, string $order = '1', string $status = 'active' ): array { return [ 'id' => $id, 'label' => $label, 'value' => $value, 'order' => $order, 'status' => $status, 'lastUpdated' => '2026-09-27T22:21:39.000Z', 'variableType' => $type, ]; } /** @return array>> The stored shape (et_divi_global_variables). */ public static function stored(): array { return [ 'numbers' => [ 'gvid-f6yq6mayfy' => self::variable('numbers', 'gvid-f6yq6mayfy', 'Eckenradius', '8px', '1'), 'gvid-5dec72fwsc' => self::variable('numbers', 'gvid-5dec72fwsc', 'Abstand Karte', '2rem', '2'), 'gvid-old0000001' => self::variable('numbers', 'gvid-old0000001', 'Alt', '1px', '', 'archived'), ], 'strings' => [ 'gvid-7uckqqhmrh' => self::variable('strings', 'gvid-7uckqqhmrh', 'Slogan', 'Handwerk', '1'), 'gvid-dup0000001' => self::variable('strings', 'gvid-dup0000001', 'Eckenradius', 'Text', '2'), ], 'fonts' => [ 'gvid-d4a5bsz2gt' => self::variable('fonts', 'gvid-d4a5bsz2gt', 'Akzentschrift', 'Merriweather'), ], 'gradients' => [ 'gvid-grad000001' => self::variable( 'gradients', 'gvid-grad000001', 'Verlauf', 'linear-gradient(red, blue)' ), ], ]; } /** @return array The shape of GlobalData::get_global_variables() (formats §2, verified in DDEV). */ public static function fromGetter(): array { $actions = ['editLabel' => true, 'editValue' => true, 'reorder' => true, 'remove' => true]; $stored = self::stored(); $withActions = static fn (array $group): array => array_map( static fn (array $entry): array => $entry + ['allowedActions' => $actions], $group ); return [ 'numbers' => (object) $withActions($stored['numbers']), 'strings' => (object) $withActions($stored['strings']), 'images' => new stdClass(), 'links' => new stdClass(), 'colors' => (object) ['gcid-x1' => ['id' => 'gcid-x1', 'label' => 'Farbe', 'value' => '#fff']], 'fonts' => (object) ([ '--et_global_heading_font' => ['id' => '--et_global_heading_font', 'label' => 'Überschrift', 'value' => 'Open Sans', 'status' => 'active', 'allowedActions' => $actions], '--et_global_body_font' => ['id' => '--et_global_body_font', 'label' => 'Fließtext', 'value' => 'Open Sans', 'status' => 'active', 'allowedActions' => $actions], ] + $withActions($stored['fonts'])), 'gradients' => (object) $withActions($stored['gradients']), ]; } public function testStoredShapeIsKeptAsItIs(): void { self::assertSame(self::stored(), VariableBook::fromDivi(self::stored())->data()); } public function testGetterShapeEqualsTheStoredShape(): void { $fromGetter = VariableBook::fromDivi(self::fromGetter()); $fromStored = VariableBook::fromDivi(self::stored()); self::assertSame($fromStored->data(), $fromGetter->data()); self::assertSame($fromStored->rev(), $fromGetter->rev()); self::assertNull($fromGetter->typeOf('--et_global_body_font')); self::assertNull($fromGetter->typeOf('gcid-x1')); self::assertArrayNotHasKey('colors', $fromGetter->data()); self::assertArrayNotHasKey('images', $fromGetter->data(), 'empty groups are dropped'); self::assertArrayNotHasKey('allowedActions', $fromGetter->entry('numbers', 'gvid-f6yq6mayfy') ?? []); } public function testGradientsPassThroughUnchanged(): void { $book = VariableBook::fromDivi(self::stored()); self::assertSame('linear-gradient(red, blue)', $book->entry('gradients', 'gvid-grad000001')['value']); self::assertSame('gradients', $book->typeOf('gvid-grad000001')); self::assertSame( 'gradients', $book->with('gradients', 'gvid-grad000001', ['label' => 'Verlauf 2', 'value' => 'x']) ->entry('gradients', 'gvid-grad000001')['variableType'] ); } public function testMissingKeysAreFilledAndIdAndTypeComeFromTheSlot(): void { $book = VariableBook::fromDivi([ 'numbers' => ['gvid-x1' => ['id' => 'other', 'label' => 'A', 'value' => 8, 'order' => null, 'variableType' => 'strings', 'extra' => 'dropped']], 'strings' => ['kein-gvid' => ['label' => 'B', 'value' => 'x']], ]); self::assertSame([ 'id' => 'gvid-x1', 'label' => 'A', 'value' => '8', 'order' => '', 'status' => 'active', 'lastUpdated' => '', 'variableType' => 'numbers', ], $book->entry('numbers', 'gvid-x1')); self::assertArrayNotHasKey('strings', $book->data()); } public function testRevIsStableAndChangesWithTheContent(): void { $book = VariableBook::fromDivi(self::stored()); $reversed = VariableBook::fromDivi(array_map( static fn (array $group): array => array_reverse($group, true), array_reverse(self::stored(), true) )); self::assertMatchesRegularExpression('/^[0-9a-f]{12}$/', $book->rev()); self::assertSame($book->rev(), $reversed->rev()); self::assertNotSame( $book->rev(), $book->with('numbers', 'gvid-f6yq6mayfy', ['label' => 'X', 'value' => '9px'])->rev() ); } public function testRealDiviFixtureAndItsGetterShapeAreTheSame(): void { $json = (string) file_get_contents(dirname(__DIR__, 4) . '/Fixtures/divi/stage4/global-variables.json'); $fixture = json_decode($json, true); $stored = VariableBook::fromDivi($fixture['stored']); $read = VariableBook::fromDivi($fixture['get_global_variables']); self::assertSame($stored->rev(), $read->rev()); self::assertSame(['numbers', 'strings', 'images', 'links', 'fonts'], array_keys($read->data())); self::assertSame('8px', $read->entry('numbers', 'gvid-f6yq6mayfy')['value']); self::assertNull($read->typeOf('--et_global_heading_font')); } public function testFindByIdAndLabel(): void { $book = VariableBook::fromDivi(self::stored()); self::assertSame(['type' => 'numbers', 'id' => 'gvid-f6yq6mayfy'], $book->find('gvid-f6yq6mayfy')); self::assertSame(['type' => 'strings', 'id' => 'gvid-7uckqqhmrh'], $book->find(' SLOGAN ')); self::assertSame(['type' => 'fonts', 'id' => 'gvid-d4a5bsz2gt'], $book->find('Akzentschrift', 'fonts')); self::assertNull($book->find('Akzentschrift', 'numbers')); self::assertNull($book->find('gvid-d4a5bsz2gt', 'numbers')); self::assertNull($book->find('Fehlt')); self::assertNull($book->find('')); } public function testAmbiguousLabelListsTheIdsAndAnArgumentTypeResolvesIt(): void { $book = VariableBook::fromDivi(self::stored()); self::assertSame(['type' => 'numbers', 'id' => 'gvid-f6yq6mayfy'], $book->find('Eckenradius', 'numbers')); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'Name der globalen Variable "Eckenradius" ist mehrdeutig; bitte die ID angeben: ' . 'gvid-f6yq6mayfy, gvid-dup0000001.' ); $book->find('Eckenradius'); } public function testWithIsImmutableAndKeepsTheTypeOrder(): void { $book = VariableBook::fromDivi(self::stored()); $added = $book->with('images', 'gvid-img0000001', ['label' => 'Bild', 'value' => '12', 'order' => '1']); self::assertCount(3, $book->data()['numbers']); self::assertSame(['numbers', 'strings', 'images', 'fonts', 'gradients'], array_keys($added->data())); self::assertSame('images', $added->entry('images', 'gvid-img0000001')['variableType']); self::assertSame('active', $added->entry('images', 'gvid-img0000001')['status']); self::assertNull($book->entry('images', 'gvid-img0000001')); } public function testWithRejectsTypesDiviDoesNotStore(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unbekannter Variablentyp "colors".'); VariableBook::fromDivi([])->with('colors', 'gvid-x1', ['label' => 'A', 'value' => '#fff']); } public function testNextOrderIsTheHighestOrderPlusOne(): void { $book = VariableBook::fromDivi(self::stored()); self::assertSame('3', $book->nextOrder('numbers'), 'the archived entry with order "" does not count'); self::assertSame('2', $book->nextOrder('fonts')); self::assertSame('1', $book->nextOrder('images')); self::assertSame('1', $book->nextOrder('colors')); self::assertSame('11', $book->with('links', 'gvid-l1', ['label' => 'L', 'value' => '/', 'order' => '10']) ->nextOrder('links')); } public function testRestoredFromKeepsCurrentOnlyVariablesArchived(): void { $current = VariableBook::fromDivi(self::stored()) ->with('numbers', 'gvid-new0000001', ['label' => 'Neu', 'value' => '4px', 'order' => '3']) ->with('images', 'gvid-img0000001', ['label' => 'Bild', 'value' => '12', 'order' => '1']) ->with('strings', 'gvid-7uckqqhmrh', ['label' => 'Slogan neu', 'value' => 'Geändert', 'order' => '1']); $snapshot = VariableBook::fromDivi(self::stored()); $restored = $current->restoredFrom($snapshot); self::assertSame('Slogan', $restored->entry('strings', 'gvid-7uckqqhmrh')['label'], 'snapshot wins'); self::assertSame( ['label' => 'Neu', 'value' => '4px', 'order' => '', 'status' => 'archived'], array_intersect_key( $restored->entry('numbers', 'gvid-new0000001') ?? [], array_flip(['status', 'order', 'label', 'value']) ) ); self::assertSame('archived', $restored->entry('images', 'gvid-img0000001')['status']); self::assertSame('', $restored->entry('images', 'gvid-img0000001')['order']); self::assertSame('archived', $restored->entry('numbers', 'gvid-old0000001')['status']); // Every ID of the current inventory survives, as Divi demands. foreach ($current->data() as $type => $entries) { foreach (array_keys($entries) as $id) { self::assertNotNull($restored->entry($type, (string) $id), $id); } } self::assertSame($snapshot->rev(), $snapshot->restoredFrom($snapshot)->rev()); } public function testRestoredArchivedEntryLosesItsOrder(): void { $current = VariableBook::fromDivi(self::stored()) ->with('fonts', 'gvid-gone000001', [ 'label' => 'Weg', 'value' => 'Lato', 'order' => '7', 'status' => 'archived', ]); $restored = $current->restoredFrom(VariableBook::fromDivi(self::stored())); self::assertSame(['archived', ''], [ $restored->entry('fonts', 'gvid-gone000001')['status'], $restored->entry('fonts', 'gvid-gone000001')['order'], ]); } public function testOnlyHereListsTheActiveVariablesARestoreWouldArchive(): void { $current = VariableBook::fromDivi(self::stored()) ->with('numbers', 'gvid-new0000001', ['label' => 'Neu', 'value' => '4px']) ->with('fonts', 'gvid-gone000001', ['label' => 'Weg', 'value' => 'Lato', 'status' => 'archived']); self::assertSame( [['type' => 'numbers', 'id' => 'gvid-new0000001', 'label' => 'Neu']], $current->onlyHere(VariableBook::fromDivi(self::stored())) ); self::assertSame([], $current->onlyHere($current)); } public function testNewIdHasTheBuilderFormat(): void { self::assertMatchesRegularExpression('/^gvid-[0-9a-z]{10}$/', VariableBook::newId()); self::assertNotSame(VariableBook::newId(), VariableBook::newId()); self::assertSame( 'gvid-zzzzzzzzzz', VariableBook::newId(static fn (int $length): string => str_repeat('z', $length)) ); self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/', VariableBook::now()); } }