Files
WP-LIT-MCP/src/Integrations/Divi/Compose/LoopScope.php
T

166 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Compose;
defined('ABSPATH') || exit;
/**
* The Divi loop a node sits in (rulings X1/X2) – an immutable value, pure PHP. Its kind comes from Divi's
* queryType: post_types and current_page → post, post_taxonomies → term, user_roles → user, menus → menu
* (the five values the Visual Builder offers; LoopUtils.php:730-739, QueryTypeController.php:89-96).
*
* "terms" and "users" are Divi's aliases for term and user loops (LoopUtils.php:881-898; LoopContext
* normalises the queryType like sanitize_key first). Any other queryType – repeater_<ACF field>, "" – 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_<post|user|term>_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<?string> $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<?string> $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<?string>
*/
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_<kind>_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;
}
}