}|null */ public static function collect( int $now = 0 ): ?array { if ( ! Module::active() ) { return null; } $now = $now > 0 ? $now : time(); $to = intdiv( $now, HOUR_IN_SECONDS ) * HOUR_IN_SECONDS; $from = $to - self::HOURS * HOUR_IN_SECONDS; $rows = self::query( $from, $to ); return null === $rows ? null : self::shape( $from, $to, $rows ); } /** * Pure shaping: rows of (hour "Y-m-d H" in UTC, status, count) → compact hourly list. * * @param int $from First covered hour (inclusive, Unix). * @param int $to End of the range (exclusive, Unix). * @param array $rows Query rows. * @return array{from: int, to: int, hours: list} */ public static function shape( int $from, int $to, array $rows ): array { $hours = array(); foreach ( $rows as $row ) { $time = strtotime( $row['hour'] . ':00:00 UTC' ); if ( false === $time || $time < $from || $time >= $to ) { continue; } if ( ! isset( $hours[ $time ] ) ) { $hours[ $time ] = array( $time, 0, 0 ); } $count = max( 0, (int) $row['total'] ); if ( self::FAILED === $row['status'] ) { $hours[ $time ][2] += $count; } elseif ( in_array( $row['status'], self::PLACED, true ) ) { $hours[ $time ][1] += $count; } } ksort( $hours ); return array( 'from' => $from, 'to' => $to, 'hours' => array_values( array_filter( $hours, static function ( array $hour ): bool { return $hour[1] > 0 || $hour[2] > 0; } ) ), ); } /** * One grouped COUNT over the order table (HPOS or posts). Read-only. * * @param int $from Range start (Unix). * @param int $to Range end (Unix). * @return array|null */ private static function query( int $from, int $to ): ?array { global $wpdb; $hpos = is_callable( array( self::ORDER_UTIL, 'custom_orders_table_usage_is_enabled' ) ) && (bool) call_user_func( array( self::ORDER_UTIL, 'custom_orders_table_usage_is_enabled' ) ); $start = gmdate( 'Y-m-d H:i:s', $from ); $end = gmdate( 'Y-m-d H:i:s', $to ); if ( $hpos ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- single read-only aggregate in the hourly cron report. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT DATE_FORMAT(date_created_gmt, %s) AS hour, status, COUNT(*) AS total FROM %i WHERE type = %s AND date_created_gmt >= %s AND date_created_gmt < %s AND status IN (%s, %s, %s, %s) GROUP BY hour, status', '%Y-%m-%d %H', $wpdb->prefix . 'wc_orders', 'shop_order', $start, $end, self::PLACED[0], self::PLACED[1], self::PLACED[2], self::FAILED ), ARRAY_A ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- see above. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT DATE_FORMAT(post_date_gmt, %s) AS hour, post_status AS status, COUNT(*) AS total FROM %i WHERE post_type = %s AND post_date_gmt >= %s AND post_date_gmt < %s AND post_status IN (%s, %s, %s, %s) GROUP BY hour, post_status', '%Y-%m-%d %H', $wpdb->posts, 'shop_order', $start, $end, self::PLACED[0], self::PLACED[1], self::PLACED[2], self::FAILED ), ARRAY_A ); } return is_array( $rows ) ? $rows : null; } }