Files
WP-LIT-MCP/src/Integrations/Divi/Pages/DynamicFields.php
T
janniclas.loosenandClaude Opus 5.5 6288acf0e6 perf: DynamicFields reads each post type's fields once per check (review M-12 b)
reason() and note() run per key and type of a body check; they now share one
offered() result per post type, so the default storage callback loads each
pod once instead of per call.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-27 23:12:14 +02:00

191 lines
7.1 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\Pages;
use Closure;
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use Throwable;
defined('ABSPATH') || exit;
/**
* The fields of a post type Divi can show as text through dynamic content (answer F4, E12): custom_meta_<key>
* reads post meta (F25/F26). Relation/file fields show IDs, Pods table storage is no post meta, protected keys
* never (S12), unknown types not (ruling P9) – each omitted with its reason. Choice fields are offered, but
* Divi shows their stored value, not the label (stage3b render: "senior") – note(). Hidden fields (security
* audit M4) are neither offered nor named. Used by lit/divi-dynamic-sources and the Theme Builder body check.
* Not final so abilities can be tested with a mock.
*/
class DynamicFields
{
/** Field types Divi can show as text (arrays of strings are joined with ", ", F26). */
public const TEXT_TYPES = ['string', 'text', 'number', 'integer', 'date', 'datetime', 'time', 'choice'];
public const ID_TYPES = ['relation', 'file'];
public const UNKNOWN = 'kein bekanntes Feld dieses Inhaltstyps';
private const PREFIX = 'custom_meta_';
/** Divi's option for a meta key typed in by hand (the key is in settings.meta_key). */
private const MANUAL = 'custom_meta_manual_custom_field_value';
/** @var Closure(string): ?string */
private Closure $podStorage;
/** @var array<string, array{0: list<array<string, string>>, 1: list<array<string, string>>}> offered() by type. */
private array $known = [];
/** @param (Closure(string): ?string)|null $podStorage Post type → Pods storage ("meta", "table"), null: none. */
public function __construct(private ContentSchema $schema, ?Closure $podStorage = null)
{
$this->podStorage = $podStorage ?? self::defaultStorage(...);
}
/** @return array{0: list<array<string, string>>, 1: list<array<string, string>>} Offered and omitted fields. */
public function offered(string $postType): array
{
$storage = ($this->podStorage)($postType);
$fields = [];
$omitted = [];
foreach ($this->schema->fields($postType) as $field) {
if ($field->status === FieldDefinition::HIDDEN) {
continue;
}
$entry = ['key' => $field->key, 'label' => $field->label];
$reason = self::omission($field, $storage);
if ($reason !== null) {
$omitted[] = $entry + ['reason' => $reason];
continue;
}
$fields[] = $entry + [
'type' => $field->type,
'source' => $field->source,
'placeholder' => '{{field:' . $field->key . '}}',
];
}
return [$fields, $omitted];
}
/**
* offered() once per post type for reason()/note() – they run per key and type of a check, and the default
* storage callback loads the pod each time (review M-12 b).
*
* @return array{0: list<array<string, string>>, 1: list<array<string, string>>}
*/
private function known(string $postType): array
{
return $this->known[$postType] ??= $this->offered($postType);
}
/** Why {{field:$key}} would not show as text for $postType; null when it does. */
public function reason(string $postType, string $key): ?string
{
[$fields, $omitted] = $this->known($postType);
foreach ($fields as $field) {
if ($field['key'] === $key) {
return null;
}
}
foreach ($omitted as $field) {
if ($field['key'] === $key) {
return $field['reason'];
}
}
return self::UNKNOWN;
}
/** What to know about an offered field of $postType ($key) that shows other text than expected; else null. */
public function note(string $postType, string $key): ?string
{
[$fields] = $this->known($postType);
foreach ($fields as $field) {
if ($field['key'] === $key && $field['type'] === 'choice') {
return 'Auswahlfeld – Divi zeigt den gespeicherten Wert, nicht die Beschriftung';
}
}
return null;
}
/**
* The meta key the content reference $value reads, in both forms Divi resolves (ruling Q11, stage3b README
* finding 12; DynamicContentOptionPostMetaKey.php:222-262): the name custom_meta_<key> (the plugin's form)
* and the name post_meta_key with select_meta_key "custom_meta_<key>" (Divi's dialog), each also with a key
* typed in by hand (custom_meta_manual_custom_field_value or post_meta_key with only meta_key). Null for
* anything else. Reads, never validates – writing goes through VariableRef::canonical() (S29/S30).
*/
public static function metaKeyOf(string $value): ?string
{
if (VariableRef::typeOf($value) !== 'content') {
return null;
}
$name = VariableRef::nameOf($value);
$settings = VariableRef::settingsOf($value);
$manual = is_string($settings['meta_key'] ?? null) ? $settings['meta_key'] : '';
if ($name === 'post_meta_key') {
$selected = $settings['select_meta_key'] ?? '';
if (! is_string($selected)) {
return null;
}
if ($selected === '') {
return $manual !== '' ? $manual : null;
}
$name = $selected;
}
if ($name === null || ! str_starts_with($name, self::PREFIX)) {
return null;
}
if ($name === self::MANUAL) {
return $manual !== '' ? $manual : null;
}
$key = str_replace(self::PREFIX, '', $name);
return $key !== '' ? $key : null;
}
private static function omission(FieldDefinition $field, ?string $storage): ?string
{
return match (true) {
! DynamicContentRules::isMetaKey($field->key)
=> 'geschütztes oder ungültiges Meta-Feld (beginnt mit „_“)',
$field->source === 'pods' && $storage === 'table'
=> 'Pods mit Tabellen-Speicherung – Divi liest nur Post-Meta',
in_array($field->type, self::ID_TYPES, true)
=> 'Beziehungs- oder Dateifeld – Divi zeigt davon nur IDs',
$field->type === 'unknown'
=> 'Feldtyp unbekannt – Divi zeigt womöglich keinen Text',
! in_array($field->type, self::TEXT_TYPES, true)
=> sprintf('Feldtyp „%s“ ist nicht textartig', $field->type),
default => null,
};
}
private static function defaultStorage(string $postType): ?string
{
if (! function_exists('pods_api')) {
return null;
}
try {
$pod = pods_api()->load_pod(['name' => $postType]);
} catch (Throwable) {
return null;
}
return is_object($pod) && method_exists($pod, 'get_storage') ? (string) $pod->get_storage() : null;
}
}