| @@ -22,8 +22,15 @@ | ||
| 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 | + /** | |
| 26 | 33 | * Recognised plugin definitions. Each entry maps a "plugin id" used in the |
| 27 | 34 | * detection result to: |
| 28 | 35 | * |
| 29 | 36 | * - basename: the plugin file basename (matches `is_plugin_active()`). |
| @@ -219,15 +226,20 @@ | ||
| 219 | 226 | '_billing_nif' => Tax_Id_Types::TYPE_ES_NIF, |
| 220 | 227 | '_billing_cuit' => Tax_Id_Types::TYPE_AR_CUIT, |
| 221 | 228 | ); |
| 222 | 229 | |
| 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. | |
| 225 | 230 | if ( ! \function_exists( 'wc_get_orders' ) ) { |
| 226 | 231 | return array(); |
| 227 | 232 | } |
| 228 | 233 | |
| 229 | - $orders = \wc_get_orders( | |
| 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( | |
| 230 | 242 | array( |
| 231 | 243 | 'limit' => $limit, |
| 232 | 244 | 'orderby' => 'date', |
| 233 | 245 | 'order' => 'DESC', |
| @@ -234,30 +246,14 @@ | ||
| 234 | 246 | 'status' => 'any', |
| 235 | 247 | 'return' => 'ids', |
| 236 | 248 | ) |
| 237 | 249 | ); |
| 238 | - if ( ! \is_array( $orders ) || empty( $orders ) ) { | |
| 250 | + $ids = array_values( array_filter( array_map( 'intval', \is_array( $ids ) ? $ids : array() ) ) ); | |
| 251 | + if ( array() === $ids ) { | |
| 239 | 252 | return array(); |
| 240 | 253 | } |
| 241 | 254 | |
| 242 | - // Tally populated rows per candidate. | |
| 243 | - $counts = array_fill_keys( array_keys( $candidates ), 0 ); | |
| 244 | - foreach ( $orders as $order_id ) { | |
| 245 | - if ( \is_object( $order_id ) && \method_exists( $order_id, 'get_id' ) ) { | |
| 246 | - $order_id = $order_id->get_id(); | |
| 247 | - } | |
| 248 | - $meta = \get_post_meta( (int) $order_id ); | |
| 249 | - if ( ! \is_array( $meta ) || empty( $meta ) ) { | |
| 250 | - continue; | |
| 251 | - } | |
| 252 | - foreach ( $candidates as $meta_key => $_type ) { | |
| 253 | - $value = isset( $meta[ $meta_key ][0] ) ? $meta[ $meta_key ][0] : null; | |
| 254 | - if ( '' === $value || array() === $value || null === $value ) { | |
| 255 | - continue; | |
| 256 | - } | |
| 257 | - ++$counts[ $meta_key ]; | |
| 258 | - } | |
| 259 | - } | |
| 255 | + $counts = self::count_populated_keys( $ids, array_keys( $candidates ) ); | |
| 260 | 256 | |
| 261 | 257 | // Pick the top-counted key per type. |
| 262 | 258 | $best = array(); |
| 263 | 259 | foreach ( $candidates as $meta_key => $type ) { |
| @@ -277,16 +273,68 @@ | ||
| 277 | 273 | return $inferred; |
| 278 | 274 | } |
| 279 | 275 | |
| 280 | 276 | /** |
| 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 | + /** | |
| 281 | 330 | * Build the full detection summary for a request. Cached per-request. |
| 282 | 331 | * |
| 283 | 332 | * @return array{plugins:array<int,string>,write_map:array<string,string>} |
| 284 | 333 | */ |
| 285 | 334 | public function summary(): array { |
| 286 | - static $cache = null; | |
| 287 | - if ( null !== $cache ) { | |
| 288 | - return $cache; | |
| 335 | + if ( null !== self::$summary_cache ) { | |
| 336 | + return self::$summary_cache; | |
| 289 | 337 | } |
| 290 | 338 | |
| 291 | 339 | $active = self::active_plugin_ids(); |
| 292 | 340 | $inferred = empty( $active ) ? self::infer_from_recent_orders() : array(); |
| @@ -292,12 +340,23 @@ | ||
| 292 | 340 | $inferred = empty( $active ) ? self::infer_from_recent_orders() : array(); |
| 293 | 341 | $overrides = Tax_Id_Settings::get_overrides(); |
| 294 | 342 | $defaults = Tax_Id_Settings::default_write_map(); |
| 295 | 343 | |
| 296 | - $cache = array( | |
| 344 | + self::$summary_cache = array( | |
| 297 | 345 | 'plugins' => $active, |
| 298 | 346 | 'write_map' => self::compose_write_map( $defaults, $inferred, $active, $overrides ), |
| 299 | 347 | ); |
| 300 | 348 | |
| 301 | - return $cache; | |
| 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; | |
| 302 | 361 | } |
| 303 | 362 | } |