= 0; $i-- ) { $sum += (int) $digits[ $i ] * $weight; $weight = 4 - $weight; } return ( 10 - $sum % 10 ) % 10; } /** * The codes a scanner's usual changes stand for, without the code itself: * 14 digits with a 0 in front (GTIN-14): the EAN-13, and with a second 0 the UPC-A; * 13 digits with a 0 in front: the UPC-A it carries; * 12 digits: the EAN-13 without its check digit, or a UPC-A stored with a 0 in front; * 11 digits: a UPC-A without its check digit (as 12 digits, or as 13 with a 0 in front); * 7 digits: an EAN-8 without its check digit. * @return string[] */ public static function forms( $code ): array { $code = self::clean( $code ); if ( ! preg_match( '/^\d{7,14}$/D', $code ) ) { return array(); } $forms = array(); $len = strlen( $code ); if ( 14 === $len && '0' === $code[0] ) { $forms[] = substr( $code, 1 ); if ( '0' === $code[1] ) { $forms[] = substr( $code, 2 ); } } if ( 13 === $len && '0' === $code[0] ) { $forms[] = substr( $code, 1 ); } if ( 12 === $len ) { $forms[] = $code . self::check_digit( $code ); $forms[] = '0' . $code; } if ( 11 === $len ) { $upc = $code . self::check_digit( $code ); $forms[] = $upc; $forms[] = '0' . $upc; } if ( 7 === $len ) { $forms[] = $code . self::check_digit( $code ); } return array_values( array_unique( array_diff( $forms, array( $code ) ) ) ); } /** * The product a scanner's changed code stands for: its id and the code stored on it, or null when no product holds one of * the codes it may have meant, when two do, or when a product holds the code exactly as it arrived (the caller's own * lookup turned that one down, so no other product is offered in its place). Products and options of any status but * trash; every caller keeps its own rules on what may be sold, counted or bought. * @return array{id:int,code:string}|null */ public static function match( $code ): ?array { $code = self::clean( $code ); $forms = self::forms( $code ); if ( ! $forms || self::holders( array( $code ), 1 ) ) { return null; } $rows = self::holders( $forms, 2 ); return 1 === count( $rows ) ? array( 'id'=>(int) key( $rows ), 'code'=>(string) current( $rows ) ) : null; } /** The id of match(), or 0. */ public static function guess( $code ): int { $hit = self::match( $code ); return $hit ? $hit['id'] : 0; } /** The code to search a list for: the code stored on the one product a scanner's changed code stands for, else the term. */ public static function term( string $term ): string { $hit = self::forms( $term ) ? self::match( $term ) : null; return $hit ? $hit['code'] : $term; } /** * Up to $limit products and options holding one of these exact codes in a barcode field (WooCommerce's GTIN, _barcode, * _ean) or the SKU: id => the code it holds. * @param string[] $codes * @return array */ private static function holders( array $codes, int $limit ): array { global $wpdb; $codes = array_values( array_filter( array_map( 'strval', $codes ), static fn( $c ) => $c !== '' ) ); if ( ! $codes ) { return array(); } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.post_id, MIN(pm.meta_value) AS code FROM %i pm JOIN %i p ON p.ID=pm.post_id WHERE pm.meta_key IN ('_global_unique_id','_barcode','_ean','_sku') AND pm.meta_value IN (" . implode( ',', array_fill( 0, count( $codes ), '%s' ) ) . ") AND p.post_type IN ('product','product_variation') AND p.post_status IN ('publish','private','draft','pending','future') GROUP BY pm.post_id LIMIT %d", array_merge( array( $wpdb->postmeta, $wpdb->posts ), $codes, array( $limit ) ) ), ARRAY_A ); $out = array(); foreach ( (array) $rows as $row ) { $out[ (int) $row['post_id'] ] = (string) $row['code']; } return $out; } }