Files
WP-LIT-MCP/src/Integrations/Pods/PodsProbe.php
T
Jan-Niclas LoosenandClaude Opus 5.5 661d3945fd feat(cache): Verhaltensprobe und Smoke für WP Super Cache
SuperCacheProbe prüft beim Prüfen, ob wp_cache_post_edit die Cache-Datei einer Testseite entfernt; während der Probe ist nur der Einzelseiten-Purge erlaubt, der Pfad kommt aus home und es werden nur feste Dateinamen gelöscht. ProbeStep bündelt die Schrittlogik von Pods und WP Super Cache; ThrowawayPost hält Marker und Titel zentral und räumt auch private Probe-Seiten auf (WP Super Cache wie Pods beim Laden des Reiters). super-cache-smoke.sh prüft gegen das echte Plugin; lib.sh sperrt parallele Smoke-Läufe per flock, wp-config.php wird atomar wiederhergestellt. Doku: Host-Abhängigkeit des Einzel-Purges, WP-Cron für den Nachlauf, Komplett-Purge durch Divi beim Prüfen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:25:37 +01:00

174 lines
6.2 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\Pods;
use LIT\MCP\Compatibility\ThrowawayPost;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Log;
use Throwable;
defined('ABSPATH') || exit;
/**
* Behaviour probe of the Pods check: through PodsFieldSource (always usable here – the probe is what frees a locked
* profile again) it loads the schema of an existing pod, writes a plain text field of a throwaway draft entry,
* reads it back and compares. The pod and its schema are never touched (no save_pod, so no rewrite flush, no
* schema cache change); only the entry is created and always deleted for good (ThrowawayPost). Entries a dead
* check left behind carry ThrowawayPost::MARKER; removeOrphans() deletes them. Runs only from the check route
* (Compatibility::check).
*
* Target: a pod of a post type Pods created itself (pod arg 'object' empty) comes first, then a foreign type Pods
* only extends (object = the type, e.g. an events plugin), last post and page. Creating and deleting the entry
* fires the hooks of other plugins on that type (unavoidable with wp_insert_post/wp_delete_post), and Pods may
* check other required fields on a partial save than the plain text field the probe writes.
*
* shortcut: relations (output ids) are not probed – they need a second pod entry; add when a Pods update breaks them.
*/
final class PodsProbe
{
public const VALUE = 'litcheck-pods-probe';
private const ABORTED = 'Pods-Probe abgebrochen (Details im PHP-Error-Log).';
private const RANK_OWN = 0;
private const RANK_EXTENDED = 1;
private const RANK_CORE = 2;
/** @var callable(string): mixed */
private $loadPod;
/** @var callable(): list<string> */
private $postTypes;
/**
* @param callable(string): mixed|null $loadPod Pods schema of a post type (raw field types).
* @param callable(): list<string>|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<string>|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<int> 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<string> */
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.'];
}
}