objectId = $objectId; $this->objectType = $objectType; $this->contentType = $contentType; if ($objectId && $objectType) { $this->meta = new MetaManager($objectId, $objectType, $contentType); $this->loadFieldDefinitions(); } $this->buildContext(); } /** * Create resolver for current queried object */ public static function forCurrentObject(): self { if (is_singular()) { $post = get_post(); if ($post) { return new self($post->ID, 'post', $post->post_type); } } elseif (is_tax() || is_category() || is_tag()) { $term = get_queried_object(); if ($term instanceof WP_Term) { return new self($term->term_id, 'term', $term->taxonomy); } } elseif (is_author()) { $author = get_queried_object(); if ($author instanceof WP_User) { return new self($author->ID, 'user', jvbUserRole($author->ID)); } } elseif (is_post_type_archive()) { // Get the post type being archived $postType = get_query_var('post_type'); if (is_array($postType)) { $postType = reset($postType); } // Create resolver with archive context (no objectId needed) return new self(null, 'archive', $postType); } // Fallback for pages without specific objects return new self(); } /** * Resolve a template string * * @param string $template Template with {{variables}} * @return string Resolved string */ public function resolve(string $template): string { return preg_replace_callback( '/\{\{([^}]+)\}\}/', fn($matches) => $this->resolveVariable($matches[1]), $template ); } /** * Resolve a single variable */ public function resolveVariable(string $variable): mixed { $variable = trim($variable); $custom = apply_filters( 'jvbSEOResolveVariable', null, $variable, $this->objectId, $this->objectType, $this->contentType, $this->meta ); if ($custom !== null) { return $this->formatValue($custom, $variable); } // Check for dot notation (relation access) if (str_contains($variable, '.')) { return $this->resolveRelation($variable); } // Check context first (site variables, etc.) if (isset($this->context[$variable])) { return $this->context[$variable]; } // Check special accessors $special = $this->resolveSpecial($variable); if ($special !== null) { return $special; } // Try to get from MetaManager if ($this->meta) { $value = $this->meta->getValue($variable); // Auto-resolve complex field types via SchemaFieldHelpers $value = $this->autoResolveField($variable, $value); return $this->formatValue($value, $variable); } // Return empty if not found return ''; } /** * Auto-resolve field via SchemaFieldHelpers (DELEGATED) * * This is the main integration point - all enhancement logic * is now handled by SchemaFieldHelpers.autoResolve() */ private function autoResolveField(string $fieldName, mixed $value): mixed { if ($value === null || $value === '') { return $value; } // Check if this is a relational field that needs a schema reference $fieldDef = $this->fieldDefinitions[$fieldName] ?? null; if ($fieldDef && $this->isRelationalField($fieldDef) && is_numeric($value)) { $objectType = $this->mapFieldTypeToObjectType($fieldDef['type']); return SchemaReferenceBuilder::build($objectType, (int)$value); } // Check if this is a term asking for related posts (e.g., shop → artists) if ($this->objectType === 'term' && $this->isRelatedPostsField($fieldName)) { return $this->buildRelatedPostsReferences($fieldName); } // Check if this is a post asking for related terms (e.g., artist → styles) if ($this->objectType === 'post' && $this->isRelatedTermsField($fieldName)) { return $this->buildRelatedTermsReferences($fieldName); } // Delegate to SchemaFieldHelpers for all other enhancement return SchemaFieldHelpers::autoResolve($fieldName, $value, $this->meta); } /** * Check if field name indicates related posts (plural form of post type) * * Examples: "artists", "artworks", "partners" */ private function isRelatedPostsField(string $fieldName): bool { if (!defined('JVB_CONTENT')) { return false; } // Check if field name matches any plural content type foreach (JVB_CONTENT as $type => $config) { $plural = strtolower($config['plural'] ?? ''); if ($plural && $fieldName === $plural) { return true; } } return false; } /** * Check if field name indicates related terms (plural form of taxonomy) * * Examples: "styles", "themes", "shops" */ private function isRelatedTermsField(string $fieldName): bool { if (!defined('JVB_TAXONOMY')) { return false; } // Check if field name matches any plural taxonomy foreach (JVB_TAXONOMY as $taxonomy => $config) { $plural = strtolower($config['plural'] ?? ''); if ($plural && $fieldName === $plural) { return true; } } return false; } /** * Build references for posts related to current term */ private function buildRelatedPostsReferences(string $fieldName): array { if ($this->objectType !== 'term' || !$this->objectId) { return []; } // Find the post type from the field name $postType = $this->getPostTypeFromPluralName($fieldName); if (!$postType) { return []; } // Build references (default: 10 items, minimal context) return SchemaReferenceBuilder::buildFromTerm( $this->objectId, $postType, limit: 10, includeContext: true ); } /** * Build references for terms related to current post */ private function buildRelatedTermsReferences(string $fieldName): array { if ($this->objectType !== 'post' || !$this->objectId) { return []; } // Find the taxonomy from the field name $taxonomy = $this->getTaxonomyFromPluralName($fieldName); if (!$taxonomy) { return []; } // Build references (default: 10 items, minimal context) return SchemaReferenceBuilder::buildFromPost( $this->objectId, $taxonomy, limit: 10, includeContext: false // Terms usually don't need context ); } /** * Get post type key from plural name */ private function getPostTypeFromPluralName(string $pluralName): ?string { if (!defined('JVB_CONTENT')) { return null; } foreach (JVB_CONTENT as $type => $config) { $plural = strtolower($config['plural'] ?? ''); if ($plural === $pluralName) { return $type; } } return null; } /** * Get taxonomy key from plural name */ private function getTaxonomyFromPluralName(string $pluralName): ?string { if (!defined('JVB_TAXONOMY')) { return null; } foreach (JVB_TAXONOMY as $taxonomy => $config) { $plural = strtolower($config['plural'] ?? ''); if ($plural === $pluralName) { return $taxonomy; } } return null; } /** * Check if field is relational (references another entity) */ private function isRelationalField(array $fieldDef): bool { return in_array($fieldDef['type'] ?? '', ['post', 'post_object', 'taxonomy', 'user']); } /** * Map field type to object type for SchemaReferenceBuilder */ private function mapFieldTypeToObjectType(string $fieldType): string { return match($fieldType) { 'post', 'post_object' => 'post', 'taxonomy' => 'term', 'user' => 'user', default => 'post' }; } /** * Resolve dot notation like {{author.name}} or {{shop.location.address}} */ private function resolveRelation(string $path): string { $parts = explode('.', $path); $relation = array_shift($parts); $field = implode('.', $parts); // Get the related object $related = $this->getRelatedObject($relation); if (!$related) { return ''; } // Create a resolver for the related object and resolve the field $relatedResolver = $this->createRelatedResolver($related); if (!$relatedResolver) { return ''; } return $relatedResolver->resolveVariable($field); } /** * Get a related object by relation name */ private function getRelatedObject(string $relation): mixed { if (!$this->meta) { return null; } // Common relations switch ($relation) { case 'author': if ($this->objectType === 'post') { $post = get_post($this->objectId); return $post ? get_user_by('id', $post->post_author) : null; } break; case 'featured_image': if ($this->objectType === 'post') { $imageId = get_post_thumbnail_id($this->objectId); return $imageId ? get_post($imageId) : null; } break; } // Check field definitions for taxonomy or post relations if (isset($this->fieldDefinitions[$relation])) { $fieldDef = $this->fieldDefinitions[$relation]; $value = $this->meta->getValue($relation); if (!$value) { return null; } switch ($fieldDef['type'] ?? '') { case 'taxonomy': // Get first term from taxonomy $taxonomy = $fieldDef['taxonomy'] ?? $relation; if ($this->objectType === 'post') { $terms = wp_get_post_terms($this->objectId, jvbCheckBase($taxonomy)); return !empty($terms) && !is_wp_error($terms) ? $terms[0] : null; } return is_numeric($value) ? get_term($value) : null; case 'post': case 'post_object': return get_post($value); case 'user': return get_user_by('id', $value); } } // Check if it's a taxonomy on the post if ($this->objectType === 'post') { $taxonomyName = jvbCheckBase($relation); if (taxonomy_exists($taxonomyName)) { $terms = wp_get_post_terms($this->objectId, $taxonomyName); return !empty($terms) && !is_wp_error($terms) ? $terms[0] : null; } } return null; } /** * Create a resolver for a related object */ private function createRelatedResolver(mixed $object): ?self { if ($object instanceof WP_Post) { return new self($object->ID, 'post', $object->post_type); } elseif ($object instanceof WP_Term) { return new self($object->term_id, 'term', $object->taxonomy); } elseif ($object instanceof WP_User) { return new self($object->ID, 'user', jvbUserRole($object->ID)); } return null; } /** * Resolve special built-in variables */ private function resolveSpecial(string $variable): ?string { // Location component accessors if (str_starts_with($variable, 'location_')) { $component = substr($variable, 9); return $this->resolveLocationComponent($component); } // Image URL accessors for different sizes if (str_ends_with($variable, '_image_url')) { $field = str_replace('_image_url', '', $variable); $imageId = $this->meta?->getValue($field); if ($imageId) { return wp_get_attachment_image_url($imageId, 'full') ?: ''; } } switch ($variable) { case 'permalink': case 'url': return $this->getObjectUrl(); case 'featured_image_url': case 'thumbnail_url': if ($this->objectType === 'post') { $url = get_the_post_thumbnail_url($this->objectId, 'full'); return $url ?: ''; } return ''; case 'post_date': case 'date_published': if ($this->objectType === 'post') { return get_the_date('c', $this->objectId); } return ''; case 'post_modified': case 'date_modified': if ($this->objectType === 'post') { return get_the_modified_date('c', $this->objectId); } return ''; case 'term_count': case 'count': if ($this->objectType === 'term') { $term = get_term($this->objectId); return $term ? (string)$term->count : '0'; } return ''; } return null; } /** * Resolve location component (e.g., location_address, location_city) */ private function resolveLocationComponent(string $component): string { $location = $this->meta?->getValue('location'); if (!is_array($location)) { return ''; } return (string)($location[$component] ?? ''); } /** * Get URL for current object */ private function getObjectUrl(): string { return match($this->objectType) { 'post' => get_permalink($this->objectId) ?: '', 'term' => is_wp_error($link = get_term_link($this->objectId)) ? '' : $link, 'user' => get_author_posts_url($this->objectId) ?: '', 'archive' => $this->contentType ? (get_post_type_archive_link(jvbCheckBase($this->contentType)) ?: '') : '', default => '' }; } /** * Format a value for output */ private function formatValue(mixed $value, string $field = ''): string { if (is_null($value) || $value === '') { return ''; } if (is_array($value)) { return $this->formatArrayValue($value, $field); } if (is_bool($value)) { return $value ? 'true' : 'false'; } return (string)$value; } /** * Format array values */ private function formatArrayValue(array $value, string $field): string { // Check if it's a repeater with sub-fields if (isset($value[0]) && is_array($value[0])) { // Extract specific field if pattern indicates $subField = $this->getArraySubField($field); if ($subField) { $extracted = array_column($value, $subField); return implode(', ', array_filter($extracted)); } // Default: try common field names foreach (['name', 'title', 'url', 'value', 'keyword', 'language'] as $key) { if (isset($value[0][$key])) { return implode(', ', array_column($value, $key)); } } } // Simple array return implode(', ', array_filter($value)); } /** * Check if field name indicates a sub-field extraction */ private function getArraySubField(string $field): ?string { // Pattern: field_name[sub_field] if (preg_match('/\[(\w+)\]$/', $field, $matches)) { return $matches[1]; } return null; } /** * Build context with site-wide variables */ private function buildContext(): void { $this->context = [ 'site_name' => get_bloginfo('name'), 'site_description' => get_bloginfo('description'), 'site_url' => get_home_url(), 'current_url' => $this->getCurrentUrl(), 'current_year' => date('Y'), 'current_date' => date('Y-m-d'), ]; // Add object-specific context if ($this->objectType === 'post' && $this->objectId) { $post = get_post($this->objectId); if ($post) { $this->context['post_title'] = $post->post_title; $this->context['post_excerpt'] = $post->post_excerpt ?: wp_trim_words($post->post_content, 20); $this->context['post_type'] = $post->post_type; } } elseif ($this->objectType === 'term' && $this->objectId) { $term = get_term($this->objectId); if ($term && !is_wp_error($term)) { $this->context['term_name'] = $term->name; $this->context['term_description'] = $term->description; $this->context['taxonomy'] = $term->taxonomy; } } elseif ($this->objectType === 'user' && $this->objectId) { $user = get_userdata($this->objectId); if ($user) { $this->context['user_name'] = $user->display_name; $this->context['user_login'] = $user->user_login; } } elseif ($this->objectType === 'archive' && $this->contentType) { // Archive-specific context $postType = jvbCheckBase($this->contentType); $postTypeObject = get_post_type_object($postType); if ($postTypeObject) { $this->context['archive_title'] = $postTypeObject->labels->name ?? ''; $this->context['archive_description'] = $postTypeObject->description ?? ''; $this->context['archive_url'] = get_post_type_archive_link($postType) ?: ''; $this->context['post_type'] = $postType; $this->context['post_type_label'] = $postTypeObject->labels->singular_name ?? ''; } } } /** * Get current URL */ private function getCurrentUrl(): string { global $wp; return home_url($wp->request); } /** * Load field definitions from content config */ private function loadFieldDefinitions(): void { if (!$this->contentType) { return; } $typeKey = str_replace(BASE, '', $this->contentType); if ($this->objectType === 'post' && defined('JVB_CONTENT')) { $this->fieldDefinitions = JVB_CONTENT[$typeKey]['fields'] ?? []; } elseif ($this->objectType === 'term' && defined('JVB_TAXONOMY')) { $this->fieldDefinitions = JVB_TAXONOMY[$typeKey]['fields'] ?? []; } elseif ($this->objectType === 'user' && defined('JVB_USER')) { $this->fieldDefinitions = JVB_USER[$typeKey]['fields'] ?? []; } } }