'metaforge_group', 'posts_per_page' => -1, 'post_status' => 'publish', ) ); foreach ( $posts as $post ) { $group = get_post_meta( $post->ID, '_metaforge_group_config', true ); if ( is_array( $group ) && ! empty( $group['fields'] ) ) { metaforge_build_flat_map_recursive( $group['fields'], $flat_map ); } } set_transient( 'metaforge_field_flat_map', $flat_map, HOUR_IN_SECONDS * 24 ); } $result = $flat_map[ $field_name ] ?? null; return $cache[ $field_name ] = $result; } function metaforge_build_flat_map_recursive( array $fields, array &$flat_map ) { foreach ( $fields as $field ) { if ( ! is_array( $field ) ) { continue; } if ( ! empty( $field['name'] ) ) { $flat_map[ $field['name'] ] = $field; } if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) { metaforge_build_flat_map_recursive( $field['sub_fields'], $flat_map ); } if ( ! empty( $field['layouts'] ) && is_array( $field['layouts'] ) ) { foreach ( $field['layouts'] as $fc_layout ) { if ( ! empty( $fc_layout['sub_fields'] ) && is_array( $fc_layout['sub_fields'] ) ) { metaforge_build_flat_map_recursive( $fc_layout['sub_fields'], $flat_map ); } } } } } // ┌─────────────────────────────────────────────────────────────────────────┐ // │ INTERNAL — Per-type value formatter │ // └─────────────────────────────────────────────────────────────────────────┘ /** * Central formatter — converts any raw DB value into the correct PHP value * based on field type. Recurses into repeaters and groups automatically. * * @param mixed $value Raw value from DB. * @param array $field Field config from metaforge_find_field_config(). * @return mixed */ function metaforge_format_value( $value, array $field ) { $type = $field['type'] ?? 'text'; if ( $type === 'repeater' ) { return metaforge_format_repeater_rows( $value, $field['sub_fields'] ?? array() ); } if ( $type === 'group' ) { return metaforge_format_group_value( $value, $field['sub_fields'] ?? array() ); } if ( class_exists( 'MetaForge_Field_Registry' ) ) { $registry_instance = MetaForge_Field_Registry::get_field_type( $type ); if ( $registry_instance ) { $reflector = new ReflectionMethod( $registry_instance, 'format_value' ); if ( $reflector->getDeclaringClass()->getName() !== 'MetaForge_Field_Type' ) { return $registry_instance->format_value( $value, 0, $field ); } } } switch ( $type ) { case 'textarea': $new_lines = isset( $field['new_lines'] ) ? $field['new_lines'] : 'br'; if ( is_string( $value ) ) { if ( $new_lines === 'wpautop' ) { return ( strpos( $value, '

' ) !== false || strpos( $value, '

(string) $value, 'title' => '', 'text' => '', 'target' => '', ); case 'date_time': return $value; case 'oembed': if ( empty( $value ) ) { return ''; } if ( is_string( $value ) && strpos( $value, ''; } return wp_oembed_get( $value ) ?: $value; case 'relationship': if ( empty( $value ) ) { return array(); } $ids = is_array( $value ) ? $value : explode( ',', (string) $value ); $ids = array_filter( array_map( 'intval', $ids ) ); if ( empty( $ids ) ) { return array(); } $fetched_posts = get_posts( array( 'post__in' => $ids, 'posts_per_page' => -1, 'post_type' => 'any', 'orderby' => 'post__in', ) ); if ( ! empty( $fetched_posts ) ) { metaforge_prime_post_cache( wp_list_pluck( $fetched_posts, 'ID' ) ); } return $fetched_posts; case 'gallery': if ( empty( $value ) ) { return array(); } $ids = is_array( $value ) ? $value : explode( ',', (string) $value ); $ids = array_filter( array_map( 'intval', $ids ) ); $format = $field['image_return_format'] ?? 'array'; $gallery = array(); foreach ( $ids as $id ) { $img = metaforge_format_image( $id, $format ); if ( $img ) { $gallery[] = $img; } } return $gallery; case 'user': if ( class_exists( 'MetaForge_UserField' ) ) { $uf = new MetaForge_UserField(); return $uf->format_value( $value, 0, $field ); } return $value; case 'flexible_content': if ( class_exists( 'MetaForge_FlexibleContentField' ) ) { $fcf = new MetaForge_FlexibleContentField(); return $fcf->format_value( $value, 0, $field ); } return metaforge_format_repeater_rows( $value, $field['sub_fields'] ?? array() ); case 'wysiwyg': return wpautop( $value ); default: return $value; } } /** * Image: attachment ID → array / url / id * * 'array' returns: * [ * 'ID' => 18, * 'id' => 18, * 'url' => 'https://example.com/.../photo.jpg', * 'alt' => 'Alt text', * 'title' => 'Photo Title', * 'caption' => '', * 'filename' => 'photo.jpg', * 'width' => 1920, * 'height' => 1080, * 'mime_type'=> 'image/jpeg', * 'sizes' => [ 'thumbnail' => ['url'=>..., 'width'=>150, 'height'=>150], ... ] * ] */ function metaforge_format_image( $attachment_id, $format = 'array' ) { if ( is_array( $attachment_id ) ) { if ( $format === 'url' ) { return $attachment_id['url'] ?? ''; } if ( $format === 'id' ) { return $attachment_id['ID'] ?? ( $attachment_id['id'] ?? 0 ); } return $attachment_id; } $attachment_id = intval( $attachment_id ); if ( ! $attachment_id ) { return ( $format === 'id' ) ? 0 : ( ( $format === 'url' ) ? '' : null ); } if ( $format === 'id' ) { return (int) $attachment_id; } if ( $format === 'url' ) { return wp_get_attachment_url( $attachment_id ) ?: ''; } // format === 'array' $post = get_post( $attachment_id ); $meta = wp_get_attachment_metadata( $attachment_id ); $full_src = wp_get_attachment_image_src( $attachment_id, 'full' ); $img_url = $full_src ? $full_src[0] : ( wp_get_attachment_url( $attachment_id ) ?: '' ); if ( ! $post || empty( $img_url ) ) { return null; } $mime = get_post_mime_type( $attachment_id ); $alt = (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); $sizes = array(); if ( ! empty( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) { $upload_dir = wp_upload_dir(); $base_url = trailingslashit( $upload_dir['baseurl'] ); $dir = isset( $meta['file'] ) ? trailingslashit( dirname( $meta['file'] ) ) : ''; foreach ( $meta['sizes'] as $size_name => $data ) { if ( ! empty( $data['file'] ) ) { $sizes[ $size_name ] = array( 'url' => $base_url . $dir . $data['file'], 'width' => (int) ( $data['width'] ?? 0 ), 'height' => (int) ( $data['height'] ?? 0 ), 'mime' => $data['mime-type'] ?? $mime, ); } } } $file_path = (string) get_attached_file( $attachment_id ); return array( 'ID' => $attachment_id, 'id' => $attachment_id, 'title' => get_the_title( $post ), 'filename' => ! empty( $file_path ) ? basename( $file_path ) : '', 'url' => $img_url, 'alt' => $alt, 'caption' => $post->post_excerpt, 'description' => $post->post_content, 'mime_type' => $mime, 'width' => ! empty( $full_src[1] ) ? (int) $full_src[1] : ( ! empty( $meta['width'] ) ? (int) $meta['width'] : 0 ), 'height' => ! empty( $full_src[2] ) ? (int) $full_src[2] : ( ! empty( $meta['height'] ) ? (int) $meta['height'] : 0 ), 'sizes' => $sizes, ); } /** File: attachment ID → structured array */ function metaforge_format_file( $attachment_id ) { if ( is_array( $attachment_id ) ) { return $attachment_id; } $attachment_id = intval( $attachment_id ); if ( ! $attachment_id ) { return null; } $post = get_post( $attachment_id ); return array( 'ID' => $attachment_id, 'id' => $attachment_id, 'title' => $post ? get_the_title( $post ) : 'Download File', 'filename' => basename( (string) get_attached_file( $attachment_id ) ), 'url' => wp_get_attachment_url( $attachment_id ) ?: '', 'mime_type' => get_post_mime_type( $attachment_id ) ?: '', 'link' => get_attachment_link( $attachment_id ) ?: '', ); } /** * Repeater: format every sub-field in every row using each sub-field's own config. * Recursion: if a sub-field is itself a group or repeater, it formats those too. */ function metaforge_format_repeater_rows( $rows, array $sub_field_configs ) { if ( ! is_array( $rows ) || empty( $rows ) ) { return array(); } $config_map = array(); foreach ( $sub_field_configs as $cfg ) { if ( ! empty( $cfg['name'] ) ) { $config_map[ $cfg['name'] ] = $cfg; } } $formatted = array(); foreach ( array_values( $rows ) as $row ) { if ( ! is_array( $row ) ) { continue; } $formatted_row = array(); foreach ( $row as $key => $val ) { if ( isset( $config_map[ $key ] ) ) { $formatted_row[ $key ] = metaforge_format_value( $val, $config_map[ $key ] ); } else { $field_cfg = metaforge_find_field_config( $key ); $formatted_row[ $key ] = $field_cfg ? metaforge_format_value( $val, $field_cfg ) : $val; } } $formatted[] = $formatted_row; } return $formatted; } /** * Group: format every sub-field using its own config. * Recursion: nested repeaters/groups inside this group are also formatted. */ function metaforge_format_group_value( $value, array $sub_field_configs ) { if ( ! is_array( $value ) ) { return $value; } $config_map = array(); foreach ( $sub_field_configs as $cfg ) { if ( ! empty( $cfg['name'] ) ) { $config_map[ $cfg['name'] ] = $cfg; } } $formatted = array(); foreach ( $value as $key => $val ) { if ( isset( $config_map[ $key ] ) ) { $formatted[ $key ] = metaforge_format_value( $val, $config_map[ $key ] ); } else { $field_cfg = metaforge_find_field_config( $key ); $formatted[ $key ] = $field_cfg ? metaforge_format_value( $val, $field_cfg ) : $val; } } return $formatted; } if ( ! function_exists( 'metaforge_format_field' ) ) { /** * Format any raw field, group, or subfield value using its registered configuration. * * @param string $field_name Field name. * @param mixed $value Raw value from postmeta or layout row. * @return mixed Formatted value (e.g. image array, formatted group, etc.) */ function metaforge_format_field( $field_name, $value = null ) { $field = metaforge_find_field_config( $field_name ); if ( ! $field ) { return $value; } return metaforge_format_value( $value, $field ); } } // ┌─────────────────────────────────────────────────────────────────────────┐ // │ PUBLIC API │ // └─────────────────────────────────────────────────────────────────────────┘ /** * Internal: Resolve raw database value from post, term, user, comment, or options store. * Supports string entity formats such as 'term_12', 'taxonomy_term_12', 'user_4', 'comment_9', 'options'. * * @param string $field_name Field name. * @param mixed $post_id Post ID or object identifier string. * @return mixed */ function metaforge_resolve_raw_meta( $field_name, $post_id = null ) { // 1. Check if we are inside a layout template with global $layout if ( isset( $GLOBALS['layout'] ) && is_array( $GLOBALS['layout'] ) && array_key_exists( $field_name, $GLOBALS['layout'] ) ) { return $GLOBALS['layout'][ $field_name ]; } // 2. Check if we are in an active metaforge loop stack (metaforge_have_rows) global $metaforge_loop_stack; if ( ! empty( $metaforge_loop_stack ) && is_array( $metaforge_loop_stack ) ) { $i = count( $metaforge_loop_stack ) - 1; $idx = $metaforge_loop_stack[ $i ]['index'] ?? -1; if ( $idx >= 0 && isset( $metaforge_loop_stack[ $i ]['rows'][ $idx ][ $field_name ] ) ) { return $metaforge_loop_stack[ $i ]['rows'][ $idx ][ $field_name ]; } } if ( $post_id === null || $post_id === false || $post_id === '' ) { $post_id = get_the_ID(); } if ( ! $post_id ) { return null; } // Handle string identifier formats if ( is_string( $post_id ) ) { $post_id_trimmed = trim( $post_id ); $post_id_lower = strtolower( $post_id_trimmed ); if ( $post_id_lower === 'options' || $post_id_lower === 'option' ) { $opt_posts = get_posts( array( 'post_type' => 'metaforge_options', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ) ); if ( ! empty( $opt_posts ) ) { foreach ( $opt_posts as $oid ) { $v = get_post_meta( $oid, $field_name, true ); if ( $v !== '' && $v !== null ) { return $v; } } } return null; } if ( strpos( $post_id_lower, 'term_' ) === 0 ) { $term_id = absint( substr( $post_id_lower, 5 ) ); return $term_id ? get_term_meta( $term_id, $field_name, true ) : null; } if ( strpos( $post_id_lower, 'taxonomy_term_' ) === 0 ) { $term_id = absint( substr( $post_id_lower, 14 ) ); return $term_id ? get_term_meta( $term_id, $field_name, true ) : null; } if ( strpos( $post_id_lower, 'user_' ) === 0 ) { $user_id = absint( substr( $post_id_lower, 5 ) ); return $user_id ? get_user_meta( $user_id, $field_name, true ) : null; } if ( strpos( $post_id_lower, 'comment_' ) === 0 ) { $comment_id = absint( substr( $post_id_lower, 8 ) ); return $comment_id ? get_comment_meta( $comment_id, $field_name, true ) : null; } } $numeric_id = absint( $post_id ); if ( ! $numeric_id ) { return null; } return get_post_meta( $numeric_id, $field_name, true ); } if ( ! function_exists( 'metaforge_get_field' ) ) { /** * metaforge_get_field( $field_name, $post_id ) * * Get any single field value — auto-formatted by type. * Supports post IDs, 'term_12', 'user_5', 'comment_3', or 'options'. * * metaforge_get_field('hero_image') → image array (or url/id per builder setting) * metaforge_get_field('category_icon', 'term_15') → formatted field for term * metaforge_get_field('bio_link', 'user_2') → formatted field for user * metaforge_get_field('layouts') → fully formatted repeater rows * metaforge_get_field('page_title') → plain string */ function metaforge_get_field( $field_name, $post_id = null ) { $value = metaforge_resolve_raw_meta( $field_name, $post_id ); $field = metaforge_find_field_config( $field_name ); return $field ? metaforge_format_value( $value, $field ) : $value; } } if ( ! function_exists( 'metaforge_the_field' ) ) { /** * metaforge_the_field( $field_name, $post_id ) * * Directly echo a formatted field value. */ function metaforge_the_field( $field_name, $post_id = null ) { $value = metaforge_get_field( $field_name, $post_id ); if ( is_null( $value ) || false === $value || '' === $value ) { return; } if ( is_array( $value ) || is_object( $value ) ) { echo esc_html( (string) wp_json_encode( $value ) ); } else { if ( strpos( (string) $value, '= 0 && isset( $metaforge_loop_stack[ $i ]['rows'][ $idx ] ) ) { $row = $metaforge_loop_stack[ $i ]['rows'][ $idx ]; } } } if ( ! is_array( $row ) ) { return null; } $value = $row[ $sub_field_name ] ?? null; $field = metaforge_find_field_config( $sub_field_name ); return $field ? metaforge_format_value( $value, $field ) : $value; } } if ( ! function_exists( 'metaforge_resolve_image' ) ) { /** * metaforge_resolve_image( $attachment_id, $format ) * * Public helper — convert any attachment ID to array / url / id on demand. */ function metaforge_resolve_image( $attachment_id, $format = 'array' ) { return metaforge_format_image( $attachment_id, $format ); } } if ( ! function_exists( 'metaforge_render_oembed' ) ) { /** * Public API: Safely render an oEmbed URL or iframe embed code. * * Supports YouTube, Vimeo, Google Maps iframes/URLs, and all standard WP oEmbed providers. * * @param mixed $value URL or raw iframe embed HTML. * @return string Sanitized embed HTML ready for echo. */ function metaforge_render_oembed( $value ) { if ( empty( $value ) ) { return ''; } $allowed_embed = array_merge( wp_kses_allowed_html( 'post' ), array( 'iframe' => array( 'src' => true, 'height' => true, 'width' => true, 'frameborder' => true, 'allowfullscreen' => true, 'style' => true, 'loading' => true, 'referrerpolicy' => true, 'title' => true, 'allow' => true, ), ) ); if ( is_string( $value ) && strpos( $value, ''; } $embed = function_exists( 'wp_oembed_get' ) ? wp_oembed_get( $value ) : false; if ( $embed ) { return wp_kses( $embed, $allowed_embed ); } if ( is_string( $value ) && filter_var( $value, FILTER_VALIDATE_URL ) ) { return '' . esc_html( $value ) . ''; } return wp_kses( (string) $value, $allowed_embed ); } } if ( ! function_exists( 'metaforge_get_option' ) ) { /** * Public API: Get any MetaForge Options Page field value. * * The store post ID is cached statically so repeated calls for the same * page slug within one request only hit the DB once instead of N times. * * @param string $field_name The field name key. * @param string $page_slug The options page slug. * @return mixed Formatted field value. */ function metaforge_get_option( $field_name, $page_slug ) { // Static slug → post ID cache to avoid N+1 get_posts() calls in template loops. static $store_id_cache = array(); if ( ! array_key_exists( $page_slug, $store_id_cache ) ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Options store post lookup query. $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = 'metaforge_options' AND post_status = 'publish' LIMIT 1", $page_slug ) ); $store_id_cache[ $page_slug ] = $post_id ? intval( $post_id ) : null; } $store_id = $store_id_cache[ $page_slug ]; if ( ! $store_id ) { return null; } $raw = get_post_meta( $store_id, $field_name, true ); $config = metaforge_find_field_config( $field_name ); return $config ? metaforge_format_value( $raw, $config ) : $raw; } } if ( ! function_exists( 'metaforge_prime_post_cache' ) ) { /** * Prime the post meta cache for a post or list of posts. * Reduces database query counts (N+1 queries) when loading multiple posts or fields. * * @param int|array $post_ids A single post ID or array of post IDs to prime. */ function metaforge_prime_post_cache( $post_ids ) { if ( empty( $post_ids ) ) { return; } $post_ids = is_array( $post_ids ) ? array_map( 'intval', $post_ids ) : array( intval( $post_ids ) ); $post_ids = array_filter( $post_ids ); if ( ! empty( $post_ids ) ) { update_meta_cache( 'post', $post_ids ); } } } if ( ! function_exists( 'metaforge_add_local_field_group' ) ) { /** * Register a local PHP-based field group. * Allows developers to define field groups directly in code. * * @param array $group_config The field group configuration. */ function metaforge_add_local_field_group( array $group_config ) { global $metaforge_local_field_groups; if ( ! is_array( $metaforge_local_field_groups ) ) { $metaforge_local_field_groups = array(); } // Set a default key if none is provided if ( empty( $group_config['key'] ) ) { $group_config['key'] = 'group_' . md5( (string) wp_json_encode( $group_config ) ); } $metaforge_local_field_groups[ $group_config['key'] ] = $group_config; } } // ┌─────────────────────────────────────────────────────────────────────────┐ // │ METAFORGE NATIVE LOOP HELPERS FOR REPEATERS & FLEXIBLE CONTENT │ // └─────────────────────────────────────────────────────────────────────────┘ $GLOBALS['metaforge_loop_stack'] = array(); if ( ! function_exists( 'metaforge_have_rows' ) ) { function metaforge_have_rows( $field_name, $post_id = null ) { global $metaforge_loop_stack; if ( ! is_array( $metaforge_loop_stack ) ) { $metaforge_loop_stack = array(); } if ( ! empty( $metaforge_loop_stack ) ) { $current_loop = &$metaforge_loop_stack[ count( $metaforge_loop_stack ) - 1 ]; $current_row = $current_loop['rows'][ $current_loop['index'] ] ?? array(); if ( isset( $current_row[ $field_name ] ) && is_array( $current_row[ $field_name ] ) ) { $sub_rows = array_values( $current_row[ $field_name ] ); $metaforge_loop_stack[] = array( 'name' => $field_name, 'rows' => $sub_rows, 'index' => -1, 'total' => count( $sub_rows ), ); return true; } } $rows = metaforge_get_field( $field_name, $post_id ); if ( ! is_array( $rows ) || empty( $rows ) ) { return false; } $rows = array_values( $rows ); $metaforge_loop_stack[] = array( 'name' => $field_name, 'rows' => $rows, 'index' => -1, 'total' => count( $rows ), ); return true; } } if ( ! function_exists( 'metaforge_the_row' ) ) { function metaforge_the_row() { global $metaforge_loop_stack; if ( empty( $metaforge_loop_stack ) ) { return false; } $i = count( $metaforge_loop_stack ) - 1; ++$metaforge_loop_stack[ $i ]['index']; if ( $metaforge_loop_stack[ $i ]['index'] >= $metaforge_loop_stack[ $i ]['total'] ) { array_pop( $metaforge_loop_stack ); return false; } return $metaforge_loop_stack[ $i ]['rows'][ $metaforge_loop_stack[ $i ]['index'] ]; } } if ( ! function_exists( 'metaforge_reset_rows' ) ) { function metaforge_reset_rows() { global $metaforge_loop_stack; $metaforge_loop_stack = array(); } } if ( ! function_exists( 'metaforge_get_row_layout' ) ) { /** * Return the layout name of the current Flexible Content row. * * @return string|false The layout name or false if not in a flexible loop. */ function metaforge_get_row_layout() { global $metaforge_loop_stack; if ( empty( $metaforge_loop_stack ) ) { return false; } $i = count( $metaforge_loop_stack ) - 1; $idx = $metaforge_loop_stack[ $i ]['index'] ?? -1; if ( $idx < 0 || ! isset( $metaforge_loop_stack[ $i ]['rows'][ $idx ] ) ) { return false; } $current_row = $metaforge_loop_stack[ $i ]['rows'][ $idx ]; if ( is_array( $current_row ) ) { return $current_row['metaforge_layout'] ?? $current_row['layout_type'] ?? $current_row['acf_fc_layout'] ?? false; } return false; } } // ┌─────────────────────────────────────────────────────────────────────────┐ // │ FRONT-END SHORTCODE: [metaforge_field name="field_name" post_id="123"] │ // └─────────────────────────────────────────────────────────────────────────┘ add_shortcode( 'metaforge_field', function ( $atts ) { $atts = shortcode_atts( array( 'name' => '', 'post_id' => null, ), $atts, 'metaforge_field' ); if ( empty( $atts['name'] ) ) { return ''; } $val = metaforge_get_field( $atts['name'], $atts['post_id'] ); if ( null === $val || false === $val || '' === $val ) { return ''; } if ( is_array( $val ) || is_object( $val ) ) { return esc_html( (string) wp_json_encode( $val ) ); } return wp_kses_post( (string) $val ); } ); // ┌─────────────────────────────────────────────────────────────────────────┐ // │ UNIVERSAL THEME COMPATIBILITY ALIASES (get_field, have_rows, etc.) │ // └─────────────────────────────────────────────────────────────────────────┘ // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Universal theme compatibility polyfills for seamless ACF theme migration. if ( ! function_exists( 'get_field' ) ) { function get_field( $field_name, $post_id = false, $format_value = true ) { return metaforge_get_field( $field_name, $post_id ); } } if ( ! function_exists( 'the_field' ) ) { function the_field( $field_name, $post_id = false ) { metaforge_the_field( $field_name, $post_id ); } } if ( ! function_exists( 'have_rows' ) ) { function have_rows( $field_name, $post_id = false ) { return metaforge_have_rows( $field_name, $post_id ); } } if ( ! function_exists( 'the_row' ) ) { function the_row() { return metaforge_the_row(); } } if ( ! function_exists( 'get_sub_field' ) ) { function get_sub_field( $field_name, $row = null ) { return metaforge_get_sub_field( $field_name, $row ); } } if ( ! function_exists( 'reset_rows' ) ) { function reset_rows() { metaforge_reset_rows(); } } // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound