From f8912c255d3308ceaf02ad0ed9f6fbc44a565ede Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 20:44:26 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20DynamicContentRules/VariableRef=20mit?= =?UTF-8?q?=20LoopScope=20=E2=80=93=20loop=5F*=20nur=20in=20passender=20Sc?= =?UTF-8?q?hleife=20(X2,=20X5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../Divi/Compose/DynamicContentRules.php | 58 +++++-- src/Integrations/Divi/Compose/VariableRef.php | 8 +- .../Divi/Compose/DynamicContentRulesTest.php | 150 ++++++++++++++++++ .../Divi/Compose/VariableRefTest.php | 117 +++++++++++--- 4 files changed, 298 insertions(+), 35 deletions(-) create mode 100644 tests/Unit/Integrations/Divi/Compose/DynamicContentRulesTest.php diff --git a/src/Integrations/Divi/Compose/DynamicContentRules.php b/src/Integrations/Divi/Compose/DynamicContentRules.php index 6ad585c..4ec7517 100644 --- a/src/Integrations/Divi/Compose/DynamicContentRules.php +++ b/src/Integrations/Divi/Compose/DynamicContentRules.php @@ -15,7 +15,8 @@ defined('ABSPATH') || exit; * * Allowed names: a non-color global variable or a theme font (no settings, DESIGN_SETTINGS, * S27); custom_meta_ with META_KEY (no leading "_", ruling S12; settings from the map entry - * of that exact name, else META_SETTINGS); or a key of the dynamic content map. + * of that exact name, else META_SETTINGS); a loop_* key of the map only inside a Divi loop of its + * kind (constructor argument $loop, rulings X2/X5 – see loopFieldsFor()); or another key of the map. * * Dynamic content map (ruling S22), constructor argument $map: * array> option name => [setting key => field type] @@ -27,7 +28,7 @@ defined('ABSPATH') || exit; * Default (null): DEFAULT_DYNAMIC_CONTENT. Task 5 builds the map from Divi's live registry * DynamicContentOptions::get_options($post_id, 'edit') – option name => its "fields" (key => * field "type"). Always, whatever the map says: "enable_html" is rejected (VariableRef), loop_* - * names only when the map names them, custom_meta_ names and the values of settings whose + * names only in a loop of their kind, custom_meta_ names and the values of settings whose * key ends in "meta_key" (meta_key, select_meta_key, loop_meta_key …) obey META_KEY after Divi's * own prefix stripping (rulings S29, S30), and no string – before or after cleaning (ruling S28) * – may contain a reference or ")$". A map of another shape fails in the constructor with @@ -86,13 +87,17 @@ final class DynamicContentRules /** @var array> */ private array $map; + private LoopScope $loop; + /** - * @param array|null $map See the class docblock. + * @param array|null $map See the class docblock. + * @param LoopScope|null $loop The Divi loop the content sits in; null = none (no loop_* names). * @throws InvalidArgumentException When the map does not have that shape. */ - public function __construct(private DesignSystem $design, ?array $map = null) + public function __construct(private DesignSystem $design, ?array $map = null, ?LoopScope $loop = null) { $this->map = $map === null ? self::DEFAULT_DYNAMIC_CONTENT : self::checkMap($map); + $this->loop = $loop ?? LoopScope::none(); } /** @@ -110,15 +115,12 @@ final class DynamicContentRules return $this->map[$name] ?? self::META_SETTINGS; } - if (isset($this->map[$name])) { - return $this->map[$name]; + if (str_starts_with($name, 'loop_')) { + return $this->loopFieldsFor($name); } - if (str_starts_with($name, 'loop_')) { - throw new InvalidArgumentException(sprintf( - 'Schleifen-Inhalt "%s" ist hier nicht verfügbar (nur innerhalb von Divi-Schleifen).', - $name - )); + if (isset($this->map[$name])) { + return $this->map[$name]; } if (in_array($name, $this->design->contentVariableIds(), true)) { @@ -128,6 +130,40 @@ final class DynamicContentRules throw VariableRef::unknownName($name, 'content'); } + /** + * A loop_* option (rulings X2/X5): only inside a Divi loop, only of the loop's kind (LoopScope::fits(), + * by the option name – Divi reads the meta of the loop object's ID whatever the object is, so a post + * meta option in a term loop would print the post with the term's ID: a security boundary, always an + * error), and only when the map names it. Its settings then follow the usual rules (meta keys S29/S30). + * + * @return array + * @throws InvalidArgumentException + */ + private function loopFieldsFor(string $name): array + { + if ($this->loop->queryType() === null) { + throw new InvalidArgumentException(sprintf( + 'Schleifen-Inhalt "%s" braucht eine Divi-Schleife in diesem Modul oder einem übergeordneten ' + . 'Element (Abschnitt, Zeile, Spalte, Gruppe).', + $name + )); + } + + if (! $this->loop->fits($name)) { + throw new InvalidArgumentException(sprintf( + 'Schleifen-Inhalt "%s" passt nicht zur Schleife dieses Elements (Typ: %s).', + $name, + $this->loop->label() + )); + } + + if (! isset($this->map[$name])) { + throw VariableRef::unknownName($name, 'content'); + } + + return $this->map[$name]; + } + /** * Cleans one content setting by its field type (rulings S21/S22). The steps, in this order: * 1. control characters off the RAW value (S32/S32a); diff --git a/src/Integrations/Divi/Compose/VariableRef.php b/src/Integrations/Divi/Compose/VariableRef.php index 43f954e..858f1d1 100644 --- a/src/Integrations/Divi/Compose/VariableRef.php +++ b/src/Integrations/Divi/Compose/VariableRef.php @@ -27,7 +27,8 @@ defined('ABSPATH') || exit; * - type "content": DynamicContentRules – the dynamic content map (ruling S22), custom_meta_ * (S12/S29/S30), non-color global variables and theme fonts (S27); it also cleans each content * setting. "enable_html" is always rejected here. Internal links post_link_url_ ({{link:…}}, - * E13a) only in URL fields. + * E13a) only in URL fields. loop_* options only inside a Divi loop of their kind (constructor + * argument $loop, rulings X2/X5); without it no loop_* name is allowed. * A dynamic content map of the wrong shape fails in the constructor with InvalidArgumentException. */ final class VariableRef @@ -47,11 +48,12 @@ final class VariableRef /** * @param array|null $dynamicContent See DynamicContentRules. + * @param LoopScope|null $loop The Divi loop of the values; null = none. * @throws InvalidArgumentException When the map does not have that shape. */ - public function __construct(private DesignSystem $design, ?array $dynamicContent = null) + public function __construct(private DesignSystem $design, ?array $dynamicContent = null, ?LoopScope $loop = null) { - $this->content = new DynamicContentRules($design, $dynamicContent); + $this->content = new DynamicContentRules($design, $dynamicContent, $loop); } /** Whether $value contains a reference anywhere (such a value may only be a whole reference). */ diff --git a/tests/Unit/Integrations/Divi/Compose/DynamicContentRulesTest.php b/tests/Unit/Integrations/Divi/Compose/DynamicContentRulesTest.php new file mode 100644 index 0000000..b025660 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Compose/DynamicContentRulesTest.php @@ -0,0 +1,150 @@ + ['before' => 'richtext', 'after' => 'richtext'], + 'loop_post_title' => ['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'], + 'loop_post_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], + 'loop_term_name' => ['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'], + 'loop_term_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], + 'loop_user_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], + 'loop_menu_text' => ['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'], + 'loop_acf_team|||name' => ['before' => 'richtext', 'after' => 'richtext'], + ]; + + private static function rules(?LoopScope $loop = null): DynamicContentRules + { + return new DynamicContentRules(DiviFixtures::designSystem(), self::MAP, $loop); + } + + private static function assertRefused(DynamicContentRules $rules, string $name, string $message): void + { + try { + $rules->fieldsFor($name); + self::fail('Accepted: ' . $name); + } catch (InvalidArgumentException $e) { + self::assertSame($message, $e->getMessage(), $name); + } + } + + /** Spec §9 / X5: without a loop every loop_* name is refused – also one the map names. */ + public function testWithoutALoopEveryLoopOptionIsRefused(): void + { + foreach ([self::rules(), self::rules(LoopScope::none())] as $rules) { + foreach (array_keys(self::MAP) as $name) { + if (! str_starts_with($name, 'loop_')) { + continue; + } + self::assertRefused( + $rules, + $name, + sprintf( + 'Schleifen-Inhalt "%s" braucht eine Divi-Schleife in diesem Modul oder einem übergeordneten ' + . 'Element (Abschnitt, Zeile, Spalte, Gruppe).', + $name + ) + ); + } + } + } + + public function testALoopOptionOfTheLoopsKindHasItsMapSettings(): void + { + self::assertSame(self::MAP['loop_post_title'], self::rules(LoopScope::ofQueryType('post_types')) + ->fieldsFor('loop_post_title')); + self::assertSame(self::MAP['loop_post_title'], self::rules(LoopScope::ofQueryType('current_page')) + ->fieldsFor('loop_post_title')); + self::assertSame(self::MAP['loop_term_name'], self::rules(LoopScope::ofQueryType('post_taxonomies')) + ->fieldsFor('loop_term_name')); + self::assertSame( + self::MAP['loop_user_meta_key_manual_custom_field'], + self::rules(LoopScope::ofQueryType('user_roles'))->fieldsFor('loop_user_meta_key_manual_custom_field') + ); + self::assertSame(self::MAP['loop_menu_text'], self::rules(LoopScope::ofQueryType('menus')) + ->fieldsFor('loop_menu_text')); + } + + /** + * Security boundary: Divi reads the meta of the loop object's ID whatever the object is – post meta + * of the post with the term's ID (DynamicContentLoopOptions.php:145-158). A hard error, not a hint. + * + * @return array + */ + public static function mismatches(): array + { + return [ + 'post meta in a term loop' => ['post_taxonomies', 'loop_post_meta_key_manual_custom_field', 'Terme'], + 'post meta in a user loop' => ['user_roles', 'loop_post_meta_key_manual_custom_field', 'Benutzer'], + 'post meta in a menu loop' => ['menus', 'loop_post_meta_key_manual_custom_field', 'Menü'], + 'user meta in a post loop' => ['post_types', 'loop_user_meta_key_manual_custom_field', 'Beiträge'], + 'term meta in a post loop' => ['current_page', 'loop_term_meta_key_manual_custom_field', 'Beiträge'], + 'term meta in a user loop' => ['user_roles', 'loop_term_meta_key_manual_custom_field', 'Benutzer'], + 'term name in a post loop' => ['post_types', 'loop_term_name', 'Beiträge'], + 'post title in a menu loop' => ['menus', 'loop_post_title', 'Menü'], + 'menu text in a term loop' => ['post_taxonomies', 'loop_menu_text', 'Terme'], + 'acf row in a post loop' => ['post_types', 'loop_acf_team|||name', 'Beiträge'], + 'post title in an acf repeater loop' => ['repeater_team', 'loop_post_title', 'repeater_team'], + 'acf row in an acf repeater loop' => ['repeater_team', 'loop_acf_team|||name', 'repeater_team'], + ]; + } + + /** @dataProvider mismatches */ + public function testAnOptionOfAnotherKindIsRefused(string $queryType, string $name, string $label): void + { + self::assertRefused( + self::rules(LoopScope::ofQueryType($queryType)), + $name, + sprintf('Schleifen-Inhalt "%s" passt nicht zur Schleife dieses Elements (Typ: %s).', $name, $label) + ); + } + + public function testALoopOptionTheMapDoesNotNameIsUnknown(): void + { + $rules = self::rules(LoopScope::ofQueryType('post_types')); + + foreach (['loop_post_date', 'loop_post_meta_key_telefon', 'loop_'] as $name) { + try { + $rules->fieldsFor($name); + self::fail('Accepted: ' . $name); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Unbekannter Name "' . $name . '"', $e->getMessage()); + } + } + // The default map (registry unavailable) names no loop option. + $this->expectExceptionMessage('Unbekannter Name "loop_post_title"'); + (new DynamicContentRules(DiviFixtures::designSystem(), null, LoopScope::ofQueryType('post_types'))) + ->fieldsFor('loop_post_title'); + } + + public function testOtherNamesAreTheSameInsideALoop(): void + { + foreach ([null, LoopScope::ofQueryType('post_taxonomies')] as $loop) { + $rules = self::rules($loop); + + self::assertSame(self::MAP['post_title'], $rules->fieldsFor('post_title')); + self::assertSame(DynamicContentRules::META_SETTINGS, $rules->fieldsFor('custom_meta_telefon')); + self::assertSame(DynamicContentRules::DESIGN_SETTINGS, $rules->fieldsFor('gvid-s1')); + foreach (['custom_meta__edit_lock', 'site_title'] as $name) { + try { + $rules->fieldsFor($name); + self::fail('Accepted: ' . $name); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('Unbekannter Name "' . $name . '"', $e->getMessage()); + } + } + } + } +} diff --git a/tests/Unit/Integrations/Divi/Compose/VariableRefTest.php b/tests/Unit/Integrations/Divi/Compose/VariableRefTest.php index d49cd1c..71a3ad5 100644 --- a/tests/Unit/Integrations/Divi/Compose/VariableRefTest.php +++ b/tests/Unit/Integrations/Divi/Compose/VariableRefTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose; use InvalidArgumentException; +use LIT\MCP\Integrations\Divi\Compose\LoopScope; use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Tests\Support\ComposeFixture; use LIT\MCP\Tests\Support\DiviFixtures; @@ -14,9 +15,14 @@ use PHPUnit\Framework\TestCase; final class VariableRefTest extends TestCase { /** @param array>|null $dynamicContent */ - private static function ref(?array $dynamicContent = null): VariableRef + private static function ref(?array $dynamicContent = null, ?LoopScope $loop = null): VariableRef { - return new VariableRef(DiviFixtures::designSystem(), $dynamicContent); + return new VariableRef(DiviFixtures::designSystem(), $dynamicContent, $loop); + } + + private static function postLoop(): LoopScope + { + return LoopScope::ofQueryType('post_types'); } private static function content(string $name, string $settings): string @@ -24,6 +30,12 @@ final class VariableRefTest extends TestCase return '$variable({"type":"content","value":{"name":"' . $name . '","settings":' . $settings . '}})$'; } + /** Divi's one post meta loop option (DynamicContentOptionLoopPostMetaKey.php:218-227). */ + private static function loopMeta(string $settings): string + { + return self::content('loop_post_meta_key_manual_custom_field', $settings); + } + /** A JSON "\uXXXX" escape, built from bytes so no tool turns it into the character itself. */ private static function escape(string $hex): string { @@ -219,7 +231,18 @@ final class VariableRefTest extends TestCase self::content('post_date', '{"date_format":"d.m.Y","offset":3}'), $ref->canonical(self::content('post_date', '{"date_format":" d.m.Y","offset":3}'), $sanitizer) ); - self::assertSame(self::content('loop_post_title', '{}'), $ref->canonical(self::content('loop_post_title', '{}'), $sanitizer)); + // Ruling X5: a loop_* name the map names still needs a loop of its kind. + try { + $ref->canonical(self::content('loop_post_title', '{}'), $sanitizer); + self::fail('loop_post_title accepted without a loop'); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString('braucht eine Divi-Schleife', $e->getMessage()); + } + $inLoop = self::ref(['loop_post_title' => []], self::postLoop()); + self::assertSame( + self::content('loop_post_title', '{}'), + $inLoop->canonical(self::content('loop_post_title', '{}'), $sanitizer) + ); // Names outside the map: custom_meta_ keeps its rule, the rest is unknown. self::assertSame( self::content('custom_meta_telefon', '{"before":"T"}'), @@ -241,6 +264,57 @@ final class VariableRefTest extends TestCase } } + /** Ruling X2/X5: loop content in a loop of its kind is cleaned like any other content reference. */ + public function testLoopContentInALoopOfItsKind(): void + { + $meta = [ + 'before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text', + 'select_loop_meta_key' => 'select', 'loop_meta_key' => 'text', 'enable_html' => 'yes_no_button', + ]; + $map = [ + 'loop_post_date' => [ + 'before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text', + 'date_format' => 'select', 'custom_date_format' => 'text', + ], + 'loop_post_meta_key_manual_custom_field' => $meta, + 'loop_term_meta_key_manual_custom_field' => $meta, + ]; + $sanitizer = ComposeFixture::sanitizer(); + $date = self::content( + 'loop_post_date', + '{"before":"Am ","after":"","loop_position":"","date_format":"custom","custom_date_format":"d.m.Y"}' + ); + + self::assertSame($date, self::ref($map, self::postLoop())->canonical($date, $sanitizer)); + self::assertSame( + self::content('loop_post_date', '{"before":"Am ","after":""}'), + self::ref($map, self::postLoop())->canonical( + self::content('loop_post_date', '{"before":"Am ","after":""}'), + $sanitizer + ) + ); + + $terms = self::ref($map, LoopScope::ofQueryType('post_taxonomies')); + $termMeta = self::content('loop_term_meta_key_manual_custom_field', '{"select_loop_meta_key":"%s"}'); + $farbe = sprintf($termMeta, 'loop_term_meta_key_farbe'); + self::assertSame($farbe, $terms->canonical($farbe, $sanitizer)); + + $rejected = [ + 'Typ: Terme' => [$terms, self::loopMeta('{"select_loop_meta_key":"loop_post_meta_key_telefon"}')], + 'Meta-Schlüssel' => [$terms, sprintf($termMeta, 'loop_term_meta_key__x')], + 'HTML-Ausgabe' => [self::ref($map, self::postLoop()), self::loopMeta('{"enable_html":"on"}')], + 'braucht eine Divi-Schleife' => [self::ref($map), $date], + ]; + foreach ($rejected as $fragment => [$ref, $value]) { + try { + $ref->canonical($value, $sanitizer); + self::fail('Accepted: ' . $value); + } catch (InvalidArgumentException $e) { + self::assertStringContainsString($fragment, $e->getMessage()); + } + } + } + /** S28: the split-marker rows really carry no marker before sanitizing. */ public function testSplitMarkerRowsHaveNoRawMarker(): void { @@ -258,8 +332,8 @@ final class VariableRefTest extends TestCase $ref = self::ref([ 'post_meta_key' => ['before' => 'text', 'meta_key' => 'text', 'select_meta_key' => 'select'], 'custom_meta__edit_lock' => ['before' => 'text'], - 'loop_post_meta_key' => ['loop_meta_key' => 'text'], - ]); + 'loop_post_meta_key_manual_custom_field' => ['loop_meta_key' => 'text'], + ], self::postLoop()); $sanitizer = ComposeFixture::sanitizer(); self::assertSame( @@ -275,7 +349,7 @@ final class VariableRefTest extends TestCase self::content('post_meta_key', '{"meta_key":"_wp_page_template"}') => 'meta_key', self::content('post_meta_key', '{"select_meta_key":"custom_meta__edit_lock"}') => 'select_meta_key', self::content('post_meta_key', '{"meta_key":"a b"}') => 'meta_key', - self::content('loop_post_meta_key', '{"loop_meta_key":"_secret"}') => 'loop_meta_key', + self::loopMeta('{"loop_meta_key":"_secret"}') => 'loop_meta_key', self::content('custom_meta__edit_lock', '{}') => 'custom_meta__edit_lock', ]; foreach ($rejected as $value => $fragment) { @@ -300,20 +374,20 @@ final class VariableRefTest extends TestCase self::content('post_meta_key', '{"select_meta_key":"custom_meta_custom_meta__wp_page_template"}'), ], 'doubled prefix in meta_key' => [self::content('post_meta_key', '{"meta_key":"custom_meta_custom_meta__x"}')], - 'loop post prefix' => [self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key__edit_lock"}')], - 'loop user prefix' => [self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_user_meta_key__session_tokens"}')], - 'loop term prefix' => [self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_term_meta_key__secret"}')], + 'loop post prefix' => [self::loopMeta('{"select_loop_meta_key":"loop_post_meta_key__edit_lock"}')], + 'loop user prefix' => [self::loopMeta('{"select_loop_meta_key":"loop_user_meta_key__session_tokens"}')], + 'loop term prefix' => [self::loopMeta('{"select_loop_meta_key":"loop_term_meta_key__secret"}')], 'doubled loop prefix' => [ - self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key_loop_post_meta_key__edit_lock"}'), + self::loopMeta('{"select_loop_meta_key":"loop_post_meta_key_loop_post_meta_key__edit_lock"}'), ], 'mixed loop and meta prefix' => [ - self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key_custom_meta__edit_lock"}'), + self::loopMeta('{"select_loop_meta_key":"loop_post_meta_key_custom_meta__edit_lock"}'), ], 'mixed meta and loop prefix' => [ self::content('post_meta_key', '{"select_meta_key":"custom_meta_loop_term_meta_key__secret"}'), ], 'two loop prefixes' => [ - self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_user_meta_key_loop_post_meta_key_telefon"}'), + self::loopMeta('{"select_loop_meta_key":"loop_user_meta_key_loop_post_meta_key_telefon"}'), ], ]; } @@ -328,10 +402,12 @@ final class VariableRefTest extends TestCase { $ref = self::ref([ 'post_meta_key' => ['meta_key' => 'text', 'select_meta_key' => 'select'], - 'loop_post_meta_key' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], - ]); + 'loop_post_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], + ], self::postLoop()); $this->expectException(InvalidArgumentException::class); + // The meta key rule refuses it – not the loop check (the post loop fits the post meta option). + $this->expectExceptionMessageMatches('/Meta-Schlüssel|Unbekannter Name/'); $ref->canonical($value, ComposeFixture::sanitizer()); } @@ -340,18 +416,17 @@ final class VariableRefTest extends TestCase { $ref = self::ref([ 'post_meta_key' => ['meta_key' => 'text', 'select_meta_key' => 'select'], - 'loop_post_meta_key' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], - ]); + 'loop_post_meta_key_manual_custom_field' => ['select_loop_meta_key' => 'select', 'loop_meta_key' => 'text'], + ], self::postLoop()); $sanitizer = ComposeFixture::sanitizer(); foreach ([ self::content('custom_meta_telefon', '{}'), self::content('post_meta_key', '{"select_meta_key":"custom_meta_fax"}'), - self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_post_meta_key_telefon"}'), - self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_user_meta_key_first_name"}'), - self::content('loop_post_meta_key', '{"select_loop_meta_key":"loop_term_meta_key_farbe"}'), - self::content( - 'loop_post_meta_key', + self::loopMeta('{"select_loop_meta_key":"loop_post_meta_key_telefon"}'), + self::loopMeta('{"select_loop_meta_key":"loop_user_meta_key_first_name"}'), + self::loopMeta('{"select_loop_meta_key":"loop_term_meta_key_farbe"}'), + self::loopMeta( '{"select_loop_meta_key":"loop_post_meta_key_manual_custom_field_value","loop_meta_key":"telefon"}' ), ] as $value) {