PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.25
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.25
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
← All changes | includes/Services/Tax_Id_Detector.php +86 -27 1.10.10 → 1.10.25 View file →
@@ -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 }