<?php
|
namespace JVBase\managers\SEO\schemas\resolvers;
|
|
use JVBase\managers\SEO\schemas\SchemaDefinition;
|
use JVBase\managers\SEO\schemas\SchemaResolverInterface;
|
use JVBase\managers\SEO\SchemaBuilder;
|
use JVBase\managers\SEO\SchemaFieldHelpers;
|
use JVBase\managers\SEO\TemplateResolver;
|
use JVBase\meta\Meta;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Default schema resolver - handles generic field resolution.
|
*
|
* Type-specific resolvers extend this and override:
|
* - getAutoFields() for auto-enrichment
|
* - resolveField() for custom field handling
|
* - transformField() for custom transformations
|
*
|
* Single transformation pass (fixes the double-transform bug).
|
*/
|
class BaseResolver implements SchemaResolverInterface
|
{
|
protected ?TemplateResolver $templateResolver = null;
|
protected ?SchemaBuilder $builder = null;
|
|
/**
|
* Properties that multi-property transformers expand into
|
* (e.g., location_complex returns both 'address' and 'geo')
|
*/
|
private const MULTI_PROPS = ['address', 'geo', 'openingHours', 'sameAs'];
|
|
public function resolve(SchemaDefinition $definition, ?Meta $meta = null): ?array
|
{
|
$schema = ['@type' => $definition->schemaType];
|
|
if ($definition->id) {
|
$schema['@id'] = $definition->id;
|
}
|
|
$this->builder = SchemaBuilder::getInstance();
|
$this->templateResolver = $this->buildTemplateResolver($definition);
|
|
// Resolve each config field (single pass)
|
foreach ($definition->config as $fieldName => $value) {
|
if ($fieldName === 'type' || $this->isEmpty($value)) {
|
continue;
|
}
|
|
$resolved = $this->resolveField($fieldName, $value, $meta);
|
|
if (!$this->isEmpty($resolved)) {
|
$schema = $this->mergeField($schema, $fieldName, $resolved);
|
}
|
}
|
|
// Auto-enrich with type-specific fields (won't override existing)
|
foreach ($this->getAutoFields($definition) as $fieldName => $value) {
|
if (!isset($schema[$fieldName]) && !$this->isEmpty($value)) {
|
$schema[$fieldName] = $value;
|
}
|
}
|
|
return count($schema) > 1 ? $schema : null;
|
}
|
|
/**
|
* Override in subclasses to add type-specific auto-fields.
|
*/
|
public function getAutoFields(SchemaDefinition $definition): array
|
{
|
return [];
|
}
|
|
/**
|
* Resolve a single field value.
|
*
|
* Template resolution → transformation in one pass.
|
* Subclasses can override for specific field handling.
|
*/
|
protected function resolveField(string $fieldName, mixed $value, ?Meta $meta): mixed
|
{
|
// Resolve template patterns ({{post_title}}, etc.)
|
$value = $this->resolveTemplates($value);
|
|
if ($value === null) {
|
return null;
|
}
|
|
// Single transformation pass
|
return $this->transformField($fieldName, $value, $meta);
|
}
|
|
/**
|
* Resolve template patterns in a value (string or nested array).
|
*
|
* @return mixed Resolved value, or null if unresolvable pattern
|
*/
|
protected function resolveTemplates(mixed $value): mixed
|
{
|
if (is_string($value) && SchemaFieldHelpers::isPattern($value)) {
|
$resolved = $this->templateResolver?->resolve($value);
|
|
// Unresolved pattern → skip
|
if ($resolved === null || SchemaFieldHelpers::isPattern($resolved)) {
|
return null;
|
}
|
|
return $resolved !== '' ? $resolved : null;
|
}
|
|
// Recurse into arrays
|
if (is_array($value)) {
|
$resolved = [];
|
foreach ($value as $key => $subValue) {
|
$sub = $this->resolveTemplates($subValue);
|
if ($sub !== null) {
|
$resolved[$key] = $sub;
|
}
|
}
|
return !empty($resolved) ? $resolved : null;
|
}
|
|
return $value;
|
}
|
|
/**
|
* Transform a field value using its registered transformer.
|
*
|
* This is the SINGLE transformation pass (fixes the old double-transform bug
|
* where autoResolve ran first, then the transformer ran again).
|
*
|
* Override in subclasses to handle specific fields differently.
|
*/
|
protected function transformField(string $fieldName, mixed $value, ?Meta $meta): mixed
|
{
|
// Already transformed (has @type) → return as-is
|
if (is_array($value) && isset($value['@type'])) {
|
return $value;
|
}
|
|
// Get transformer from field definition
|
$fieldDef = $this->builder?->getFieldDefinition($fieldName);
|
|
if ($fieldDef && !empty($fieldDef['transformer'])) {
|
return $this->applyTransformer($value, $fieldDef['transformer'], $fieldName);
|
}
|
|
// Fall back to auto-resolve for fields without explicit transformers
|
return SchemaFieldHelpers::autoResolve($fieldName, $value, $meta);
|
}
|
|
/**
|
* Apply a named transformer from SchemaFieldHelpers.
|
*/
|
protected function applyTransformer(mixed $value, string $transformer, string $fieldName): mixed
|
{
|
if (!method_exists(SchemaFieldHelpers::class, $transformer)) {
|
return $value;
|
}
|
|
try {
|
return SchemaFieldHelpers::$transformer($value);
|
} catch (\Throwable $e) {
|
error_log("Schema transformer error for {$fieldName}: {$e->getMessage()}");
|
return $value;
|
}
|
}
|
|
/**
|
* Merge a resolved field into the schema array.
|
*
|
* Handles multi-property returns (e.g., location_complex → address + geo).
|
*/
|
protected function mergeField(array $schema, string $fieldName, mixed $value): array
|
{
|
// Check for multi-property expansion
|
if (is_array($value) && !isset($value['@type']) && !isset($value[0])) {
|
if (!empty(array_intersect(array_keys($value), self::MULTI_PROPS))) {
|
foreach ($value as $subKey => $subValue) {
|
if (!$this->isEmpty($subValue)) {
|
$schema[$subKey] = $subValue;
|
}
|
}
|
return $schema;
|
}
|
}
|
|
$schema[$fieldName] = $value;
|
return $schema;
|
}
|
|
/**
|
* Build the TemplateResolver for this definition's context.
|
*/
|
protected function buildTemplateResolver(SchemaDefinition $definition): ?TemplateResolver
|
{
|
if ($definition->objectId && $definition->objectType) {
|
return new TemplateResolver(
|
$definition->objectId,
|
$definition->objectType,
|
$definition->contentType
|
);
|
}
|
|
return TemplateResolver::forCurrentObject();
|
}
|
|
/**
|
* Build a Meta instance for the definition's context.
|
*/
|
protected function buildMeta(SchemaDefinition $definition): ?Meta
|
{
|
if (!$definition->objectId || !$definition->objectType) {
|
return null;
|
}
|
|
return new Meta($definition->objectId, $definition->objectType);
|
}
|
|
protected function isEmpty(mixed $value): bool
|
{
|
return $value === null || $value === '' || $value === [];
|
}
|
}
|