feat: Pods field source for schema detection and writes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-25 22:13:41 +02:00
co-authored by Claude Opus 5.5
parent b3f4b7ba65
commit 4a652240a3
3 changed files with 378 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Pods;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use LIT\MCP\Integrations\WordPress\Schema\FieldSource;
use RuntimeException;
use Throwable;
/**
* Pods field adapter (spec §6.1/§6.2): definitions from pods_api()->load_pod(),
* values read and written through the Pods API so relationships and caches stay
* consistent. Works with Pods 3.x (Pods\Whatsit\Pod / Pods\Whatsit\Field).
*/
final class PodsFieldSource implements FieldSource
{
private const TYPES = [
'text' => 'string', 'website' => 'string', 'phone' => 'string', 'email' => 'string',
'password' => 'string', 'slug' => 'string', 'color' => 'string', 'oembed' => 'string',
'paragraph' => 'text', 'wysiwyg' => 'text', 'code' => 'text',
'number' => 'number', 'currency' => 'number',
'boolean' => 'boolean',
'date' => 'date', 'datetime' => 'datetime', 'time' => 'time',
'file' => 'file', 'avatar' => 'file',
];
private const LAYOUT_TYPES = ['heading', 'html'];
private const ID_TYPES = ['relation', 'file'];
/** @var callable(string): mixed */
private $loadPod;
/** @var callable(string, int): mixed */
private $podFor;
public function __construct(?callable $loadPod = null, ?callable $podFor = null)
{
$this->loadPod = $loadPod ?? static fn (string $name): mixed => pods_api()->load_pod(['name' => $name]);
$this->podFor = $podFor ?? static fn (string $name, int $id): mixed => pods($name, $id);
}
public static function isAvailable(): bool
{
return function_exists('pods_api') && function_exists('pods');
}
public function id(): string
{
return 'pods';
}
public function fieldsFor(string $postType): array
{
$pod = ($this->loadPod)($postType);
if (! is_object($pod) || $pod->get_type() !== 'post_type') {
return [];
}
$fields = [];
foreach ($pod->get_fields() as $field) {
$type = (string) $field->get_type();
if (in_array($type, self::LAYOUT_TYPES, true)) {
continue;
}
$fields[] = $this->definition($field, $type);
}
return $fields;
}
public function read(int $postId, FieldDefinition $field): mixed
{
$pod = ($this->podFor)((string) get_post_type($postId), $postId);
if (! is_object($pod)) {
return null;
}
if (in_array($field->type, self::ID_TYPES, true)) {
$ids = $pod->field(['name' => $field->key, 'output' => 'ids', 'single' => ! $field->multiple]);
if ($field->multiple) {
return array_values(array_map('intval', array_filter((array) $ids, 'is_numeric')));
}
return is_numeric($ids) ? (int) $ids : null;
}
return $pod->field(['name' => $field->key, 'single' => true, 'raw' => true]);
}
public function write(int $postId, string $postType, array $values, array $definitions): void
{
$data = [];
foreach ($values as $key => $value) {
$multiple = isset($definitions[$key]) && $definitions[$key]->multiple;
$data[$key] = $value ?? ($multiple ? [] : '');
}
$pod = ($this->podFor)($postType, $postId);
if (! is_object($pod)) {
throw new RuntimeException(sprintf('Pods object for %s #%d is not available.', $postType, $postId));
}
try {
$result = $pod->save($data, null, null, ['error_mode' => 'exception']);
} catch (Throwable $e) {
throw new RuntimeException('Pods could not save the fields: ' . $e->getMessage(), 0, $e);
}
if ($result === false || is_wp_error($result)) {
throw new RuntimeException('Pods could not save the fields.');
}
}
private function definition(object $field, string $type): FieldDefinition
{
$normalized = self::TYPES[$type] ?? 'unknown';
$choices = null;
$related = null;
if ($type === 'pick') {
if ((string) $field->get_arg('pick_object', '') === 'custom-simple') {
$normalized = 'choice';
$choices = $this->choices((string) $field->get_arg('pick_custom', ''));
} else {
$normalized = 'relation';
$name = $field->get_related_object_name();
$related = [
'object_type' => (string) $field->get_related_object_type(),
'name' => $name === null ? null : (string) $name,
];
}
} elseif ($normalized === 'file') {
$related = ['object_type' => 'media', 'name' => null];
}
return new FieldDefinition(
(string) $field->get_name(),
(string) $field->get_label(),
$normalized,
'pods',
$normalized === 'unknown' ? FieldDefinition::UNKNOWN : FieldDefinition::WRITABLE,
(bool) $field->is_required(),
(bool) $field->is_multi_value(),
$choices,
$related,
(string) $field->get_arg('description', '')
);
}
/** @return list<array{value: string, label: string}> */
private function choices(string $raw): array
{
$choices = [];
foreach (preg_split('/\r\n|\r|\n/', trim($raw)) ?: [] as $line) {
$line = trim(trim($line), '|');
if ($line === '') {
continue;
}
$parts = explode('|', $line, 2);
$value = trim($parts[0]);
$choices[] = ['value' => $value, 'label' => trim($parts[1] ?? $value)];
}
return $choices;
}
}