*/ private $postTypes; /** * @param callable(string): mixed|null $loadPod Pods schema of a post type (raw field types). * @param callable(): list|null $postTypes Post types to look at; the best-ranked target wins, then the * first of them. */ public function __construct( private PodsFieldSource $source, ?callable $loadPod = null, ?callable $postTypes = null ) { $this->loadPod = $loadPod ?? static fn (string $name): mixed => pods_api()->load_pod(['name' => $name]); $this->postTypes = $postTypes ?? static fn (): array => array_values(array_unique([ ...array_values(get_post_types(['_builtin' => false])), 'post', 'page', ])); } /** @return list|null German problems, [] = green; null = no pod with a plain text field to probe. */ public function run(): ?array { try { $target = $this->target(); } catch (Throwable $e) { Log::failure('pods probe', $e); return [self::ABORTED]; } if ($target === null) { return null; } [$postType, $field] = $target; return self::throwaway()->run($postType, fn (int $id): array => $this->exercise($id, $postType, $field)); } /** @return list IDs of probe entries a dead check left behind, deleted now. */ public static function removeOrphans(): array { return self::throwaway()->removeOrphans(array_values(get_post_types())); } private static function throwaway(): ThrowawayPost { return new ThrowawayPost( ThrowawayPost::MARKER, ThrowawayPost::TITLE, self::ABORTED, 'Der Probe-Eintrag konnte nicht angelegt werden (Details im PHP-Error-Log).', 'Der Probe-Eintrag %d konnte nicht gelöscht werden; bitte endgültig löschen (Titel „' . ThrowawayPost::TITLE . ' …“).', 'pods probe' ); } /** @return array{0: string, 1: FieldDefinition}|null Post type and field of the best-ranked text pod. */ private function target(): ?array { $best = null; $bestRank = PHP_INT_MAX; foreach (($this->postTypes)() as $postType) { $pod = ($this->loadPod)($postType); $field = is_object($pod) ? $this->textField($postType, $pod) : null; if ($field === null) { continue; } $rank = self::rank($postType, $pod); if ($rank === self::RANK_OWN) { return [$postType, $field]; } if ($rank < $bestRank) { [$best, $bestRank] = [[$postType, $field], $rank]; } } return $best; } private static function rank(string $postType, object $pod): int { if ((string) $pod->get_arg('object', '') === '') { return self::RANK_OWN; } return in_array($postType, ['post', 'page'], true) ? self::RANK_CORE : self::RANK_EXTENDED; } private function textField(string $postType, object $pod): ?FieldDefinition { $definitions = []; foreach ($this->source->fieldsFor($postType) as $definition) { $definitions[$definition->key] = $definition; } foreach ($pod->get_fields() as $field) { // Raw type 'text' only: email, slug, website … validate or rewrite the value. $definition = $definitions[(string) $field->get_name()] ?? null; if ( $definition !== null && (string) $field->get_type() === 'text' && $definition->status === FieldDefinition::WRITABLE && ! $definition->multiple ) { return $definition; } } return null; } /** @return list */ private function exercise(int $id, string $postType, FieldDefinition $field): array { $this->source->write($id, $postType, [$field->key => self::VALUE], [$field->key => $field]); $read = $this->source->read($id, $field); if ($read === self::VALUE) { return []; } // The field name and the value stay in the log: the report reaches MCP clients. Log::line(sprintf( 'pods probe: field %s read back %s instead of the written value.', $field->key, is_scalar($read) ? '"' . $read . '"' : gettype($read) )); return ['Gelesener Wert weicht vom geschriebenen ab.']; } }