$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 === []; } }