'scalar', 'title' => 'scalar', 'post_content' => 'html', 'content' => 'html', 'post_excerpt' => 'scalar', 'excerpt' => 'scalar', 'post_featured_image' => 'url', 'featured_image' => 'url', 'post_author' => 'scalar', 'author' => 'scalar', 'post_date' => 'scalar', 'date' => 'scalar', 'post_categories' => 'array', 'categories' => 'array', 'post_tags' => 'array', 'tags' => 'array', 'post_url' => 'url', 'url' => 'url', 'permalink' => 'url', 'related_posts' => 'array', ]; /** * Fallback values for when a source resolves to empty */ const FALLBACK_VALUES = [ 'post_title' => '(Untitled)', 'title' => '(Untitled)', 'post_featured_image' => '', 'featured_image' => '', 'post_excerpt' => '', 'excerpt' => '', 'related_posts' => [], ]; /** * Get expected type for a source * * @param string $source Placeholder source * @return string Type: scalar, html, url, array */ public static function get_source_type($source) { if (isset(self::SOURCE_TYPES[$source])) { return self::SOURCE_TYPES[$source]; } // Custom field types if (strpos($source, '_field_') !== false) { return 'scalar'; } // Direct key - guess based on naming if (strpos($source, 'image') !== false || strpos($source, 'thumbnail') !== false) { return 'url'; } if (strpos($source, 'ids') !== false || strpos($source, 'tags') !== false) { return 'array'; } return 'scalar'; } /** * Validate resolved value matches expected type * * @param mixed $value Resolved value * @param string $expected_type Expected type * @return bool True if valid */ public static function validate_value_type($value, $expected_type) { switch ($expected_type) { case 'scalar': case 'html': return is_string($value) || is_numeric($value); case 'url': return is_string($value) && !empty($value); case 'array': return is_array($value); default: return true; } } /** * Get fallback value for a source * * @param string $source Placeholder source * @param string $expected_type Expected type * @return mixed Fallback value */ public static function get_fallback($source, $expected_type = null) { if ($expected_type === 'array') { return []; } if (isset(self::FALLBACK_VALUES[$source])) { return self::FALLBACK_VALUES[$source]; } return ''; } /** * Check if a source is array-based (needs special handling) * * @param string $source Placeholder source * @return bool */ public static function is_array_source($source) { return self::get_source_type($source) === 'array'; } /** * Get cached post data * * @param int $post_id Post ID * @return array|null Cached post data or null if not cached */ public static function get_cached_post_data($post_id) { $cache_key = "wn_post_data_{$post_id}"; return get_transient($cache_key); } /** * Cache post data * * @param int $post_id Post ID * @param array $data Post data */ public static function cache_post_data($post_id, $data) { $cache_key = "wn_post_data_{$post_id}"; set_transient($cache_key, $data, self::CACHE_TIMEOUT); } /** * Invalidate post data cache * * @param int $post_id Post ID */ public static function invalidate_post_data_cache($post_id) { $cache_key = "wn_post_data_{$post_id}"; delete_transient($cache_key); } /** * Get value for a dynamic placeholder source * * @param string $source Placeholder source (e.g., 'post_title', 'post_content') * @param array $post_data Post data array * @param array $options Resolution options * @return mixed Resolved value */ public static function get_value($source, $post_data, $options = []) { $options = wp_parse_args($options, [ 'use_fallback' => true, 'validate_type' => true, ]); if (empty($source) || empty($post_data)) { return $options['use_fallback'] ? self::get_fallback($source) : ''; } $expected_type = $options['validate_type'] ? self::get_source_type($source) : 'scalar'; $value = ''; // Standard post fields switch ($source) { case 'post_title': case 'title': $value = $post_data['title'] ?? $post_data['post_title'] ?? ''; break; case 'post_content': case 'content': $value = $post_data['content'] ?? $post_data['post_content'] ?? ''; break; case 'post_excerpt': case 'excerpt': $value = $post_data['excerpt'] ?? $post_data['post_excerpt'] ?? ''; break; case 'post_featured_image': case 'featured_image': $value = $post_data['featured_image'] ?? $post_data['thumbnail'] ?? $post_data['_thumbnail_url'] ?? ''; break; case 'post_author': case 'author': $value = $post_data['author'] ?? $post_data['post_author'] ?? ''; break; case 'post_date': case 'date': $value = $post_data['date'] ?? $post_data['post_date'] ?? ''; break; case 'post_categories': case 'categories': $value = $post_data['categories'] ?? []; break; case 'post_tags': case 'tags': $value = $post_data['tags'] ?? []; break; case 'post_url': case 'url': case 'permalink': $value = $post_data['url'] ?? $post_data['permalink'] ?? ''; break; default: // Check for custom meta fields (format: {cpt}_field_{name}) if (strpos($source, '_field_') !== false) { $parts = explode('_field_', $source); $field_name = end($parts); // Try to get from meta array if (isset($post_data['meta'][$field_name])) { $value = $post_data['meta'][$field_name]; } elseif (isset($post_data[$field_name])) { // Try direct field access $value = $post_data[$field_name]; } } elseif (strpos($source, 'related_posts') !== false) { // Handle related_posts through the dedicated method $value = self::get_related_posts_for_post_data($post_data); } elseif (isset($post_data[$source])) { // Check for direct key match $value = $post_data[$source]; } break; } // Validate type if enabled if ($options['validate_type'] && !self::validate_value_type($value, $expected_type)) { $value = $options['use_fallback'] ? self::get_fallback($source, $expected_type) : ''; } // Apply fallback for empty values if enabled if ($options['use_fallback'] && $value === '') { $value = self::get_fallback($source, $expected_type); } return $value; } /** * Get related posts from post_data array * * @param array $post_data Post data array (must contain 'id' and 'type') * @param int $count Number of related posts * @return array Related posts data */ public static function get_related_posts_for_post_data($post_data, $count = 3) { $post_id = $post_data['id'] ?? 0; $post_type = $post_data['type'] ?? 'post'; if (!$post_id) { return []; } return self::get_related_posts($post_id, $count, $post_type); } /** * Build post data array from WP_Post object * * @param \WP_Post|int $post Post object or ID * @return array Post data array */ public static function build_post_data($post) { if (is_numeric($post)) { $post = get_post($post); } if (!$post || !($post instanceof \WP_Post)) { return []; } $data = [ 'id' => $post->ID, 'title' => $post->post_title, 'content' => apply_filters('the_content', $post->post_content), 'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 30), 'date' => get_the_date('', $post), 'date_iso' => get_the_date('c', $post), 'url' => get_permalink($post), 'slug' => $post->post_name, 'type' => $post->post_type, ]; // Author $author_id = $post->post_author; $data['author'] = get_the_author_meta('display_name', $author_id); $data['author_url'] = get_author_posts_url($author_id); // Featured image $thumbnail_id = get_post_thumbnail_id($post); if ($thumbnail_id) { $data['featured_image'] = get_the_post_thumbnail_url($post, 'large'); $data['featured_image_id'] = $thumbnail_id; } // Taxonomies $taxonomies = get_object_taxonomies($post->post_type); foreach ($taxonomies as $taxonomy) { $terms = get_the_terms($post, $taxonomy); if ($terms && !is_wp_error($terms)) { $data[$taxonomy] = array_map(function($term) { return [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'url' => get_term_link($term), ]; }, $terms); } } // Shortcuts for common taxonomies if (isset($data['category'])) { $data['categories'] = $data['category']; } if (isset($data['post_tag'])) { $data['tags'] = $data['post_tag']; } // Custom meta fields $meta = get_post_meta($post->ID); if ($meta) { $data['meta'] = []; foreach ($meta as $key => $values) { // Skip internal meta keys if (strpos($key, '_') === 0) { continue; } $data['meta'][$key] = count($values) === 1 ? $values[0] : $values; } } return $data; } /** * Get related posts * * @param int $post_id Current post ID * @param int $count Number of related posts * @param string $post_type Post type * @return array Related posts data */ public static function get_related_posts($post_id, $count = 3, $post_type = 'post') { // Get categories of current post $categories = get_the_category($post_id); $category_ids = wp_list_pluck($categories, 'term_id'); $args = [ 'post_type' => $post_type, 'posts_per_page' => $count, 'post__not_in' => [$post_id], 'orderby' => 'date', 'order' => 'DESC', ]; if (!empty($category_ids)) { $args['category__in'] = $category_ids; } $query = new \WP_Query($args); $related = []; foreach ($query->posts as $post) { $related[] = [ 'id' => $post->ID, 'title' => $post->post_title, 'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 20), 'url' => get_permalink($post), 'featured_image' => get_the_post_thumbnail_url($post, 'medium'), 'date' => get_the_date('', $post), ]; } wp_reset_postdata(); return $related; } }