Files
WP-LIT-MCP/src/Integrations/WordPress/Schema/ContentSchema.php
T
2026-09-30 00:54:48 +02:00

194 lines
5.9 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\WordPress\Schema;
use LIT\MCP\Integrations\Divi\Schema\DiviModules;
use LogicException;
defined('ABSPATH') || exit;
/**
* Detects the content model of this installation (spec §6.1): post types with a
* UI, their taxonomies and their fields merged from all field sources.
* Not final so abilities can be unit-tested with a mock.
*/
class ContentSchema
{
/** Post types handled elsewhere (media tools, Divi tools, WordPress internals). */
public const EXCLUDED = [
'attachment', 'nav_menu_item', 'revision', 'wp_block', 'wp_template', 'wp_template_part',
'wp_navigation', 'wp_global_styles', 'wp_font_family', 'wp_font_face',
...DiviModules::INTERNAL_POST_TYPES,
];
private const COUNTED_STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
/** @var callable(string): bool */
private $diviEnabled;
/** @var array<string, array<string, FieldDefinition>> */
private array $fieldCache = [];
/**
* @param list<FieldSource> $sources Highest priority first.
*/
public function __construct(private array $sources, ?callable $diviEnabled = null)
{
$this->diviEnabled = $diviEnabled ?? DiviModules::builderEnabledFor(...);
}
/** @return list<string> */
public function postTypeNames(): array
{
$excluded = apply_filters('lit_mcp_excluded_post_types', self::EXCLUDED);
$excluded = is_array($excluded) ? $excluded : self::EXCLUDED;
$names = array_values(array_diff(
array_map('strval', array_keys(get_post_types(['show_ui' => true], 'names'))),
$excluded
));
sort($names);
return $names;
}
public function isManaged(string $postType): bool
{
return in_array($postType, $this->postTypeNames(), true);
}
/** @return list<array<string, mixed>> */
public function describePostTypes(): array
{
$types = [];
foreach ($this->postTypeNames() as $name) {
$description = $this->describe($name, false);
if ($description !== null) {
$types[] = $description;
}
}
return $types;
}
/** @return array<string, mixed>|null Null when the post type is not managed. */
public function describe(string $postType, bool $withFields): ?array
{
$object = $this->isManaged($postType) ? get_post_type_object($postType) : null;
if ($object === null) {
return null;
}
$description = [
'name' => $postType,
'label' => (string) $object->labels->name,
'singular_label' => (string) $object->labels->singular_name,
'hierarchical' => (bool) $object->hierarchical,
'supports' => array_map('strval', array_keys(get_all_post_type_supports($postType))),
'divi' => $this->diviEnabled($postType),
'count' => $this->count($postType),
'taxonomies' => $this->taxonomies($postType),
];
if ($withFields) {
$visible = array_filter(
$this->fields($postType),
static fn (FieldDefinition $field): bool => $field->status !== FieldDefinition::HIDDEN
);
$description['fields'] = array_values(array_map(
static fn (FieldDefinition $field): array => $field->toArray(),
$visible
));
}
return $description;
}
/** @return array<string, FieldDefinition> */
public function fields(string $postType): array
{
if (! isset($this->fieldCache[$postType])) {
$merged = [];
foreach ($this->sources as $source) {
foreach ($source->fieldsFor($postType) as $field) {
$merged[$field->key] ??= $field;
}
}
ksort($merged);
$this->fieldCache[$postType] = $merged;
}
return $this->fieldCache[$postType];
}
public function sourceOf(FieldDefinition $field): FieldSource
{
foreach ($this->sources as $source) {
if ($source->id() === $field->source) {
return $source;
}
}
throw new LogicException(sprintf('No field source "%s" registered.', $field->source));
}
/**
* Keys of fields that store attachment IDs (file fields, relations to media) over all
* managed post types, sorted – used by the media usage search.
*
* @return list<string>
*/
public function mediaFieldKeys(): array
{
$keys = [];
foreach ($this->postTypeNames() as $postType) {
foreach ($this->fields($postType) as $field) {
if ($field->type === 'file' || ($field->related['object_type'] ?? null) === 'media') {
$keys[$field->key] = true;
}
}
}
$keys = array_map('strval', array_keys($keys));
sort($keys);
return $keys;
}
/** @return list<array{name: string, label: string, hierarchical: bool}> */
public function taxonomies(string $postType): array
{
$result = [];
foreach (get_object_taxonomies($postType, 'objects') as $taxonomy) {
if (! $taxonomy->show_ui) {
continue;
}
$result[] = [
'name' => (string) $taxonomy->name,
'label' => (string) $taxonomy->labels->name,
'hierarchical' => (bool) $taxonomy->hierarchical,
];
}
return $result;
}
public function diviEnabled(string $postType): bool
{
return ($this->diviEnabled)($postType);
}
private function count(string $postType): int
{
$counts = (array) wp_count_posts($postType);
$total = 0;
foreach (self::COUNTED_STATUSES as $status) {
$total += (int) ($counts[$status] ?? 0);
}
return $total;
}
}