assets
2 weeks ago
sources
2 months ago
class-ecs-drb-mapper.php
2 months ago
class-ecs-drb-sources.php
2 months ago
class-ecs-dynamic-repeater-module.php
2 weeks ago
class-ecs-drb-mapper.php
151 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Dynamic Repeater Builder — Mapper |
| 4 | * |
| 5 | * Converts source rows into Elementor-compatible repeater rows |
| 6 | * using a field mapping configuration. |
| 7 | */ |
| 8 | |
| 9 | if ( ! defined( 'ABSPATH' ) ) { |
| 10 | exit; |
| 11 | } |
| 12 | |
| 13 | class ECS_DRB_Mapper { |
| 14 | |
| 15 | /** |
| 16 | * Resolve source rows into Elementor-compatible repeater rows. |
| 17 | * |
| 18 | * @param array $source_rows Raw rows from a source. |
| 19 | * @param array $mapping Array of mapping rules. |
| 20 | * @param array $field_types Map of repeater_field_key => Elementor control type. |
| 21 | * e.g. [ 'link' => 'url', 'image' => 'media' ] |
| 22 | * @return array |
| 23 | */ |
| 24 | public static function resolve( array $source_rows, array $mapping, array $field_types = [] ): array { |
| 25 | $active_mapping = array_filter( $mapping, fn( $r ) => ! empty( $r['repeater_field'] ) && ! empty( $r['source_field'] ) ); |
| 26 | |
| 27 | $result = []; |
| 28 | foreach ( $source_rows as $source_row ) { |
| 29 | $row = []; |
| 30 | foreach ( $active_mapping as $rule ) { |
| 31 | $resolved = self::resolve_field( $source_row, $rule ); |
| 32 | if ( null !== $resolved ) { |
| 33 | $field_key = $rule['repeater_field']; |
| 34 | $base_key = preg_replace( '/\[(id|url)\]$/', '', $field_key ); |
| 35 | // Sub-key rows (image[id], image[url]) must stay as raw strings — |
| 36 | // their wrapping into ['id'=>N,'url'=>'...'] happens at the merge step. |
| 37 | $field_type = ( $field_key !== $base_key ) |
| 38 | ? 'text' |
| 39 | : ( $field_types[ $field_key ] ?? 'text' ); |
| 40 | $row[ $field_key ] = self::wrap_value( $resolved, $field_type ); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // Merge image sub-keys: 'img[id]' + 'img[url]' → 'img' => ['id' => N, 'url' => '...'] |
| 45 | $merged = []; |
| 46 | foreach ( $row as $key => $val ) { |
| 47 | if ( preg_match( '/^(.+)\[(id|url)\]$/', $key, $m ) ) { |
| 48 | $base = $m[1]; |
| 49 | $sub = $m[2]; |
| 50 | if ( ! isset( $merged[ $base ] ) ) { |
| 51 | $merged[ $base ] = [ 'id' => 0, 'url' => '' ]; |
| 52 | } |
| 53 | $merged[ $base ][ $sub ] = $sub === 'id' ? intval( $val ) : (string) $val; |
| 54 | } else { |
| 55 | $merged[ $key ] = $val; |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | if ( ! empty( $merged ) ) { |
| 60 | $result[] = $merged; |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | return $result; |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Wrap a resolved string value into the format Elementor expects |
| 69 | * for the given control type. |
| 70 | * |
| 71 | * @param string $value Resolved string value. |
| 72 | * @param string $field_type Elementor control type (url, media, image, text, …). |
| 73 | * @return string|array |
| 74 | */ |
| 75 | private static function wrap_value( string $value, string $field_type ) { |
| 76 | switch ( $field_type ) { |
| 77 | case 'url': |
| 78 | return [ |
| 79 | 'url' => $value, |
| 80 | 'is_external' => '', |
| 81 | 'nofollow' => '', |
| 82 | 'custom_attributes' => '', |
| 83 | ]; |
| 84 | |
| 85 | case 'media': |
| 86 | case 'image': |
| 87 | // Numeric value → treat as attachment ID and resolve the URL. |
| 88 | if ( is_numeric( $value ) && intval( $value ) > 0 ) { |
| 89 | $id = intval( $value ); |
| 90 | $url = wp_get_attachment_url( $id ) ?: ''; |
| 91 | return [ 'id' => $id, 'url' => $url ]; |
| 92 | } |
| 93 | // URL string → use directly with id = 0. |
| 94 | return [ 'id' => 0, 'url' => $value ]; |
| 95 | |
| 96 | default: |
| 97 | return $value; |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Resolve a single field from a source row. |
| 103 | * Returns null when the value is empty and skip_if_empty is true, |
| 104 | * signalling that the field should be omitted from the row entirely. |
| 105 | */ |
| 106 | private static function resolve_field( array $source_row, array $rule ): ?string { |
| 107 | $source_field = $rule['source_field'] ?? ''; |
| 108 | $before = $rule['before'] ?? ''; |
| 109 | $after = $rule['after'] ?? ''; |
| 110 | $fallback = $rule['fallback'] ?? ''; |
| 111 | $skip_if_empty = (bool) ( $rule['skip_if_empty'] ?? false ); |
| 112 | |
| 113 | // Template mode: "{post_title} by {post_author}" — replace each {key}. |
| 114 | if ( strpos( $source_field, '{' ) !== false ) { |
| 115 | $value = preg_replace_callback( |
| 116 | '/\{([a-zA-Z0-9_]+)\}/', |
| 117 | function ( $m ) use ( $source_row ) { |
| 118 | $raw = $source_row[ $m[1] ] ?? ''; |
| 119 | if ( is_array( $raw ) ) { |
| 120 | $raw = $raw['url'] ?? $raw['ID'] ?? implode( ', ', array_filter( $raw, 'is_scalar' ) ); |
| 121 | } |
| 122 | return (string) $raw; |
| 123 | }, |
| 124 | $source_field |
| 125 | ); |
| 126 | $value = trim( $value ); |
| 127 | } else { |
| 128 | // Simple field key. |
| 129 | $value = $source_row[ $source_field ] ?? ''; |
| 130 | if ( is_array( $value ) ) { |
| 131 | $value = $value['url'] ?? $value['ID'] ?? implode( ', ', array_filter( $value, 'is_scalar' ) ); |
| 132 | } |
| 133 | $value = trim( (string) $value ); |
| 134 | } |
| 135 | |
| 136 | if ( $value === '' ) { |
| 137 | if ( $fallback !== '' ) { |
| 138 | // Use fallback, but do NOT apply before/after around it — |
| 139 | // fallback is meant as a literal replacement, not a formatted value. |
| 140 | return $fallback; |
| 141 | } |
| 142 | if ( $skip_if_empty ) { |
| 143 | return null; |
| 144 | } |
| 145 | return ''; |
| 146 | } |
| 147 | |
| 148 | return $before . $value . $after; |
| 149 | } |
| 150 | } |
| 151 |