, "" – is a * loop the tools do not support: queryType() names it, but it has no kind and is not active(), so every * loop_* option is refused inside it (fail closed). ACF repeater rows would need the setting acf_type, * which the content rules refuse (ruling X7e). * * Nested loops (audit M1): an empty inner loop hands its block unchanged to the outer loop, which then * resolves the references with ITS object. So kinds() lists the kinds of all enabled loops of the chain, * and a meta key option has to fit every one of them (fits()). * * kindOfOption() is the kind an option NAME belongs to: Divi takes the meta type of * loop__meta_key_* from the name and reads the meta of the loop object's ID, whatever the * object is (DynamicContentLoopOptions.php:145-158) – a post meta option in a term loop reads the post * with the term's ID. fits() is that security check (Global Constraints: "Sicherheitsgrenze Typabgleich"). */ final class LoopScope { public const POST = 'post'; public const TERM = 'term'; public const USER = 'user'; public const MENU = 'menu'; /** Divi queryType => kind. */ private const KINDS = [ 'post_types' => self::POST, 'current_page' => self::POST, 'post_taxonomies' => self::TERM, 'user_roles' => self::USER, 'menus' => self::MENU, 'terms' => self::TERM, 'users' => self::USER, ]; private const LABELS = [ self::POST => 'Beiträge', self::TERM => 'Terme', self::USER => 'Benutzer', self::MENU => 'Menü', ]; /** Name prefix => kind of a loop option; every other loop_* name is a post option (loop_post_*, loop_product_*). */ private const OPTION_PREFIXES = [ 'loop_term_' => self::TERM, 'loop_user_' => self::USER, 'loop_menu_' => self::MENU, 'loop_acf_' => null, ]; /** @param list $outerKinds Kinds of the enclosing enabled loops, innermost first */ private function __construct(private ?string $queryType, private array $outerKinds = []) { } public static function none(): self { return new self(null); } public static function ofQueryType(string $queryType): self { return new self($queryType); } /** * The same loop inside the loops of $outerKinds (innermost first; null = an unsupported loop). * * @param list $outerKinds */ public function inside(array $outerKinds): self { return new self($this->queryType, $outerKinds); } /** * Kinds of all enabled loops of the chain, innermost (own) first; null for an unsupported loop; [] without. * * @return list */ public function kinds(): array { return $this->queryType === null ? [] : [$this->kind(), ...$this->outerKinds]; } /** * Identity of the context: the loop and the kinds of its whole chain (audit M1). Two scopes with the same key * resolve every loop_* reference the same way; no loop is "-|". */ public function key(): string { return ($this->queryType ?? '-') . '|' . implode(',', array_map( static fn (?string $kind): string => $kind ?? '?', $this->kinds() )); } /** Whether there is a loop whose loop_* options the tools allow. */ public function active(): bool { return $this->kind() !== null; } /** POST, TERM, USER or MENU; null without a loop or in an unsupported one. */ public function kind(): ?string { return $this->queryType === null ? null : (self::KINDS[$this->queryType] ?? null); } /** Divi's queryType of the loop; null without a loop. */ public function queryType(): ?string { return $this->queryType; } /** German name of the kind for messages; the raw queryType of an unsupported loop; "" without a loop. */ public function label(): string { $kind = $this->kind(); return $kind === null ? (string) $this->queryType : self::LABELS[$kind]; } /** * Whether the loop_* option $option belongs to this loop's kind – a meta key option (loop__meta_key_*) * to the kind of every loop of the chain (audit M1), since Divi reads meta by the ID of whatever loop * object resolves it. */ public function fits(string $option): bool { $kind = self::kindOfOption($option); if (! $this->active() || $kind !== $this->kind()) { return false; } return ! str_contains($option, '_meta_key_') || array_unique($this->kinds()) === [$kind]; } /** Kind of a loop_* option by its name; null for other names and ACF repeater options. */ public static function kindOfOption(string $option): ?string { if (! str_starts_with($option, 'loop_')) { return null; } foreach (self::OPTION_PREFIXES as $prefix => $kind) { if (str_starts_with($option, $prefix)) { return $kind; } } return self::POST; } }