| @@ -22,15 +22,8 @@ | ||
| 22 | 22 | * Tax_Id_Detector class. |
| 23 | 23 | */ |
| 24 | 24 | class Tax_Id_Detector { |
| 25 | 25 | /** |
| 26 | - * Per-request detection summary, built once per request by {@see summary()}. | |
| 27 | - * | |
| 28 | - * @var null|array{plugins:array<int,string>,write_map:array<string,string>} | |
| 29 | - */ | |
| 30 | - private static $summary_cache = null; | |
| 31 | - | |
| 32 | - /** | |
| 33 | 26 | * Recognised plugin definitions. Each entry maps a "plugin id" used in the |
| 34 | 27 | * detection result to: |
| 35 | 28 | * |
| 36 | 29 | * - basename: the plugin file basename (matches `is_plugin_active()`). |
| @@ -226,34 +219,42 @@ | ||
| 226 | 219 | '_billing_nif' => Tax_Id_Types::TYPE_ES_NIF, |
| 227 | 220 | '_billing_cuit' => Tax_Id_Types::TYPE_AR_CUIT, |
| 228 | 221 | ); |
| 229 | 222 | |
| 223 | + // Best-effort SQL: tolerate environments where wc_get_orders() / HPOS aren't | |
| 224 | + // available. We use the wc_get_orders() API for cross-store compatibility. | |
| 230 | 225 | if ( ! \function_exists( 'wc_get_orders' ) ) { |
| 231 | 226 | return array(); |
| 232 | 227 | } |
| 233 | 228 | |
| 234 | - /** | |
| 235 | - * Ids only, then one grouped count over the meta table. Hydrating the | |
| 236 | - * orders loaded every meta row of the newest 200 into memory on each POS | |
| 237 | - * order write and exhausted a 128 MB request on a legacy-storage store. | |
| 238 | - * | |
| 239 | - * @var array<int, int|string>|mixed $ids The stub over-narrows every wc_get_orders() result to WC_Order[]. | |
| 240 | - */ | |
| 241 | - $ids = \wc_get_orders( | |
| 229 | + $orders = \wc_get_orders( | |
| 242 | 230 | array( |
| 243 | 231 | 'limit' => $limit, |
| 244 | 232 | 'orderby' => 'date', |
| 245 | 233 | 'order' => 'DESC', |
| 246 | 234 | 'status' => 'any', |
| 247 | - 'return' => 'ids', | |
| 248 | 235 | ) |
| 249 | 236 | ); |
| 250 | - $ids = array_values( array_filter( array_map( 'intval', \is_array( $ids ) ? $ids : array() ) ) ); | |
| 251 | - if ( array() === $ids ) { | |
| 237 | + if ( ! \is_array( $orders ) || empty( $orders ) ) { | |
| 252 | 238 | return array(); |
| 253 | 239 | } |
| 254 | 240 | |
| 255 | - $counts = self::count_populated_keys( $ids, array_keys( $candidates ) ); | |
| 241 | + // Tally populated rows per candidate. | |
| 242 | + $counts = array_fill_keys( array_keys( $candidates ), 0 ); | |
| 243 | + foreach ( $orders as $order ) { | |
| 244 | + // Read meta through the order object: with HPOS on (and sync off) it lives | |
| 245 | + // in wc_orders_meta, where get_post_meta() finds nothing. | |
| 246 | + if ( ! \is_object( $order ) || ! \method_exists( $order, 'get_meta' ) ) { | |
| 247 | + continue; | |
| 248 | + } | |
| 249 | + foreach ( $candidates as $meta_key => $_type ) { | |
| 250 | + $value = $order->get_meta( $meta_key ); | |
| 251 | + if ( '' === $value || array() === $value || null === $value ) { | |
| 252 | + continue; | |
| 253 | + } | |
| 254 | + ++$counts[ $meta_key ]; | |
| 255 | + } | |
| 256 | + } | |
| 256 | 257 | |
| 257 | 258 | // Pick the top-counted key per type. |
| 258 | 259 | $best = array(); |
| 259 | 260 | foreach ( $candidates as $meta_key => $type ) { |
| @@ -273,68 +274,16 @@ | ||
| 273 | 274 | return $inferred; |
| 274 | 275 | } |
| 275 | 276 | |
| 276 | 277 | /** |
| 277 | - * How many of the given orders carry a non-empty value for each meta key. | |
| 278 | - * | |
| 279 | - * Reads the order meta table of the active datastore directly: under HPOS | |
| 280 | - * the rows live in `wc_orders_meta`, otherwise in `wp_postmeta`, the same | |
| 281 | - * split {@see Pos_Uuid::get_order_ids_by_uuid()} makes. | |
| 282 | - * | |
| 283 | - * @param int[] $ids Order ids to inspect. | |
| 284 | - * @param string[] $keys Candidate meta keys. | |
| 285 | - * | |
| 286 | - * @return array<string,int> Populated-order count per key, zero when absent. | |
| 287 | - */ | |
| 288 | - private static function count_populated_keys( array $ids, array $keys ): array { | |
| 289 | - global $wpdb; | |
| 290 | - | |
| 291 | - $counts = array_fill_keys( $keys, 0 ); | |
| 292 | - if ( array() === $ids || array() === $keys ) { | |
| 293 | - return $counts; | |
| 294 | - } | |
| 295 | - | |
| 296 | - $order_util = '\\Automattic\\WooCommerce\\Utilities\\OrderUtil'; | |
| 297 | - $hpos = class_exists( $order_util ) | |
| 298 | - && method_exists( $order_util, 'custom_orders_table_usage_is_enabled' ) | |
| 299 | - && call_user_func( array( $order_util, 'custom_orders_table_usage_is_enabled' ) ); | |
| 300 | - $table = $hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->postmeta; | |
| 301 | - $id_column = $hpos ? 'order_id' : 'post_id'; | |
| 302 | - | |
| 303 | - $id_placeholders = implode( ',', array_fill( 0, \count( $ids ), '%d' ) ); | |
| 304 | - $key_placeholders = implode( ',', array_fill( 0, \count( $keys ), '%s' ) ); | |
| 305 | - | |
| 306 | - // "Populated" means what the order getter used to decode as non-empty: a | |
| 307 | - // plugin that initialises a key with an empty array, an empty string or | |
| 308 | - // null stores `a:0:{}`, `s:0:"";` or `N;`, and those must not count. | |
| 309 | - $empty_values = array( '', 'a:0:{}', 's:0:"";', 'N;' ); | |
| 310 | - $empty_placeholders = implode( ',', array_fill( 0, \count( $empty_values ), '%s' ) ); | |
| 311 | - | |
| 312 | - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table and column names are fixed above; every value goes through a placeholder. | |
| 313 | - $sql = "SELECT meta_key, COUNT(DISTINCT {$id_column}) AS populated FROM {$table}" | |
| 314 | - . " WHERE {$id_column} IN ({$id_placeholders}) AND meta_key IN ({$key_placeholders})" | |
| 315 | - . " AND meta_value NOT IN ({$empty_placeholders})" | |
| 316 | - . ' GROUP BY meta_key'; | |
| 317 | - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 318 | - $rows = $wpdb->get_results( $wpdb->prepare( $sql, array_merge( $ids, $keys, $empty_values ) ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepared here with the placeholders built above. | |
| 319 | - | |
| 320 | - foreach ( (array) $rows as $row ) { | |
| 321 | - if ( isset( $counts[ $row['meta_key'] ] ) ) { | |
| 322 | - $counts[ $row['meta_key'] ] = (int) $row['populated']; | |
| 323 | - } | |
| 324 | - } | |
| 325 | - | |
| 326 | - return $counts; | |
| 327 | - } | |
| 328 | - | |
| 329 | - /** | |
| 330 | 278 | * Build the full detection summary for a request. Cached per-request. |
| 331 | 279 | * |
| 332 | 280 | * @return array{plugins:array<int,string>,write_map:array<string,string>} |
| 333 | 281 | */ |
| 334 | 282 | public function summary(): array { |
| 335 | - if ( null !== self::$summary_cache ) { | |
| 336 | - return self::$summary_cache; | |
| 283 | + static $cache = null; | |
| 284 | + if ( null !== $cache ) { | |
| 285 | + return $cache; | |
| 337 | 286 | } |
| 338 | 287 | |
| 339 | 288 | $active = self::active_plugin_ids(); |
| 340 | 289 | $inferred = empty( $active ) ? self::infer_from_recent_orders() : array(); |
| @@ -340,23 +289,12 @@ | ||
| 340 | 289 | $inferred = empty( $active ) ? self::infer_from_recent_orders() : array(); |
| 341 | 290 | $overrides = Tax_Id_Settings::get_overrides(); |
| 342 | 291 | $defaults = Tax_Id_Settings::default_write_map(); |
| 343 | 292 | |
| 344 | - self::$summary_cache = array( | |
| 293 | + $cache = array( | |
| 345 | 294 | 'plugins' => $active, |
| 346 | 295 | 'write_map' => self::compose_write_map( $defaults, $inferred, $active, $overrides ), |
| 347 | 296 | ); |
| 348 | 297 | |
| 349 | - return self::$summary_cache; | |
| 350 | - } | |
| 351 | - | |
| 352 | - /** | |
| 353 | - * Discard the per-request summary cache. Tests only: the PHPUnit process | |
| 354 | - * never ends between cases, so a warm cache would hide whether a write | |
| 355 | - * path asks the detector at all. | |
| 356 | - * | |
| 357 | - * @internal | |
| 358 | - */ | |
| 359 | - public static function reset_request_state(): void { | |
| 360 | - self::$summary_cache = null; | |
| 298 | + return $cache; | |
| 361 | 299 | } |
| 362 | 300 | } |