| @@ -6,8 +6,10 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace SpringDevs\Subscription\Illuminate\Plans; |
| 9 | 9 | |
| 10 | +use SpringDevs\Subscription\Illuminate\Multilingual; | |
| 11 | + | |
| 10 | 12 | /** |
| 11 | 13 | * Plan data-access layer. |
| 12 | 14 | * |
| 13 | 15 | * Read/write repository over the three plan tables (subscrpt_plan_group, |
| @@ -152,30 +154,36 @@ | ||
| 152 | 154 | if ( ! $product_id ) { |
| 153 | 155 | return array(); |
| 154 | 156 | } |
| 155 | 157 | |
| 156 | - $cache_key = 'product_' . $product_id; | |
| 157 | - $resolved = wp_cache_get( $cache_key, self::CACHE_GROUP ); | |
| 158 | + // A translation is a different post from the product its plans were | |
| 159 | + // attached to, so read across every language of it. One cache entry | |
| 160 | + // serves them all. Without WPML / Polylang this is just the product. | |
| 161 | + $product_ids = Multilingual::translation_ids( $product_id ); | |
| 162 | + $cache_key = 'product_' . Multilingual::canonical_id( $product_id ); | |
| 163 | + $resolved = wp_cache_get( $cache_key, self::CACHE_GROUP ); | |
| 158 | 164 | |
| 159 | 165 | if ( false === $resolved ) { |
| 160 | - $batch = self::resolve_for_products( array( $product_id ) ); | |
| 161 | - $resolved = $batch[ $product_id ] ?? array(); | |
| 166 | + $batch = self::resolve_for_products( $product_ids ); | |
| 167 | + $resolved = count( $product_ids ) > 1 ? self::merge_translation_rows( $batch ) : ( $batch[ $product_id ] ?? array() ); | |
| 162 | 168 | wp_cache_set( $cache_key, $resolved, self::CACHE_GROUP ); |
| 163 | 169 | } |
| 164 | 170 | |
| 165 | 171 | if ( $variation_id ) { |
| 166 | - $variation_id = absint( $variation_id ); | |
| 167 | - $resolved = array_values( | |
| 172 | + $variation_ids = Multilingual::translation_ids( $variation_id ); | |
| 173 | + $resolved = array_values( | |
| 168 | 174 | array_filter( |
| 169 | 175 | $resolved, |
| 170 | - static function ( $row ) use ( $variation_id ) { | |
| 176 | + static function ( $row ) use ( $variation_ids ) { | |
| 171 | 177 | // vid 0 = applies to the parent / all variations. |
| 172 | - return 0 === (int) $row['vid'] || $variation_id === (int) $row['vid']; | |
| 178 | + return 0 === (int) $row['vid'] || in_array( (int) $row['vid'], $variation_ids, true ); | |
| 173 | 179 | } |
| 174 | 180 | ) |
| 175 | 181 | ); |
| 176 | 182 | } |
| 177 | 183 | |
| 184 | + $resolved = Multilingual::translate_plan_rows( $resolved ); | |
| 185 | + | |
| 178 | 186 | /** |
| 179 | 187 | * Filter the resolved plan rows for a product. |
| 180 | 188 | * |
| 181 | 189 | * Pro's extension point to inject taxonomy- and variation-resolved rows. |
| @@ -263,8 +271,42 @@ | ||
| 263 | 271 | return $map; |
| 264 | 272 | } |
| 265 | 273 | |
| 266 | 274 | /** |
| 275 | + * Merge the plan rows of every language of one product into one list. | |
| 276 | + * | |
| 277 | + * A plan attached in two languages would otherwise be offered twice, so a | |
| 278 | + * plan appears once per variation it applies to, in group → plan order. | |
| 279 | + * | |
| 280 | + * @param array<int, array> $batch Map of product id → rows, from {@see resolve_for_products()}. | |
| 281 | + * | |
| 282 | + * @return array | |
| 283 | + */ | |
| 284 | + private static function merge_translation_rows( array $batch ) { | |
| 285 | + $merged = array(); | |
| 286 | + | |
| 287 | + foreach ( $batch as $rows ) { | |
| 288 | + foreach ( $rows as $row ) { | |
| 289 | + $vid = (int) $row['vid'] ? Multilingual::canonical_id( $row['vid'] ) : 0; | |
| 290 | + $key = (int) $row['plan_id'] . ':' . $vid; | |
| 291 | + | |
| 292 | + if ( ! isset( $merged[ $key ] ) ) { | |
| 293 | + $merged[ $key ] = $row; | |
| 294 | + } | |
| 295 | + } | |
| 296 | + } | |
| 297 | + | |
| 298 | + usort( | |
| 299 | + $merged, | |
| 300 | + static function ( $a, $b ) { | |
| 301 | + return array( (int) $a['plan_group_id'], (int) $a['plan_id'] ) <=> array( (int) $b['plan_group_id'], (int) $b['plan_id'] ); | |
| 302 | + } | |
| 303 | + ); | |
| 304 | + | |
| 305 | + return $merged; | |
| 306 | + } | |
| 307 | + | |
| 308 | + /** | |
| 267 | 309 | * Decode a JSON column to an array, tolerating null / plain strings. |
| 268 | 310 | * |
| 269 | 311 | * @param string|null $value Raw column value. |
| 270 | 312 | * |
| @@ -306,14 +348,17 @@ | ||
| 306 | 348 | $relation_table = self::relation_table(); |
| 307 | 349 | $plan_table = self::plan_table(); |
| 308 | 350 | $group_table = self::group_table(); |
| 309 | 351 | |
| 310 | - $where = 'r.type = %d AND r.oid = %d'; | |
| 311 | - $params = array( self::REL_PRODUCT, $product_id ); | |
| 352 | + // Every language of the product, as the storefront resolver reads them. | |
| 353 | + $product_ids = Multilingual::translation_ids( $product_id ); | |
| 354 | + $where = 'r.type = %d AND r.oid IN (' . implode( ',', array_fill( 0, count( $product_ids ), '%d' ) ) . ')'; | |
| 355 | + $params = array_merge( array( self::REL_PRODUCT ), $product_ids ); | |
| 312 | 356 | |
| 313 | 357 | if ( $variation_id ) { |
| 314 | - $where .= ' AND r.vid = %d'; | |
| 315 | - $params[] = absint( $variation_id ); | |
| 358 | + $variation_ids = Multilingual::translation_ids( $variation_id ); | |
| 359 | + $where .= ' AND r.vid IN (' . implode( ',', array_fill( 0, count( $variation_ids ), '%d' ) ) . ')'; | |
| 360 | + $params = array_merge( $params, $variation_ids ); | |
| 316 | 361 | } |
| 317 | 362 | |
| 318 | 363 | // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare |
| 319 | 364 | $rows = $wpdb->get_results( |
| @@ -419,8 +464,30 @@ | ||
| 419 | 464 | return $row ? self::decode_plan_row( $row ) : null; |
| 420 | 465 | } |
| 421 | 466 | |
| 422 | 467 | /** |
| 468 | + * Every non-empty group and plan title, any status, for string translation. | |
| 469 | + * | |
| 470 | + * @return array{groups:array<int,string>,plans:array<int,string>} Titles by id. | |
| 471 | + */ | |
| 472 | + public static function get_titles() { | |
| 473 | + global $wpdb; | |
| 474 | + | |
| 475 | + $group_table = self::group_table(); | |
| 476 | + $plan_table = self::plan_table(); | |
| 477 | + | |
| 478 | + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery | |
| 479 | + $groups = $wpdb->get_results( "SELECT id, title FROM {$group_table} WHERE title <> '' ORDER BY id ASC", ARRAY_A ); | |
| 480 | + $plans = $wpdb->get_results( "SELECT id, title FROM {$plan_table} WHERE title <> '' ORDER BY id ASC", ARRAY_A ); | |
| 481 | + // phpcs:enable | |
| 482 | + | |
| 483 | + return array( | |
| 484 | + 'groups' => array_column( (array) $groups, 'title', 'id' ), | |
| 485 | + 'plans' => array_column( (array) $plans, 'title', 'id' ), | |
| 486 | + ); | |
| 487 | + } | |
| 488 | + | |
| 489 | + /** | |
| 423 | 490 | * Fetch relations for a plan term. |
| 424 | 491 | * |
| 425 | 492 | * @param int $plan_id Plan id. |
| 426 | 493 | * |
| @@ -1023,8 +1090,10 @@ | ||
| 1023 | 1090 | $product_id = absint( $product_id ); |
| 1024 | 1091 | |
| 1025 | 1092 | if ( $product_id ) { |
| 1026 | 1093 | wp_cache_delete( 'product_' . $product_id, self::CACHE_GROUP ); |
| 1094 | + // The key every translation of the product shares, when that is another id. | |
| 1095 | + wp_cache_delete( 'product_' . Multilingual::canonical_id( $product_id ), self::CACHE_GROUP ); | |
| 1027 | 1096 | return; |
| 1028 | 1097 | } |
| 1029 | 1098 | |
| 1030 | 1099 | // No targeted product: drop the whole group when the backend supports it. |