diff --git a/includes/API/Customers_Controller.php b/includes/API/Customers_Controller.php index 0097aea..a897278 100644 --- a/includes/API/Customers_Controller.php +++ b/includes/API/Customers_Controller.php @@ -68,7 +68,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque * Optimised query for getting all customer IDs. */ if ( $request->get_param( 'posts_per_page' ) == -1 && $request->get_param( 'fields' ) !== null ) { - return $this->wcpos_get_all_posts( $request->get_param( 'fields' ) ); + return $this->wcpos_get_all_posts( $request ); } return $dispatch_result; @@ -236,17 +236,23 @@ function ( $meta ) { * multisite would return all users from all sites, not just the current site. * Also, querying by role is not as simple as querying by post type. * - * @param array $fields Fields to return. Default is ID. + * @param WP_REST_Request $request Full details about the request. * - * @return array|WP_Error + * @return WP_REST_Response|WP_Error */ - public function wcpos_get_all_posts( array $fields = array() ): array { + public function wcpos_get_all_posts( $request ) { global $wpdb; + // Start timing execution + $start_time = microtime( true ); + + $modified_after = $request->get_param( 'modified_after' ); + $dates_are_gmt = true; + $fields = $request->get_param( 'fields' ); $id_with_modified_date = array( 'id', 'date_modified_gmt' ) === $fields; $args = array( - 'fields' => array( 'ID', 'registered' ), // Return only the ID and registered date. + 'fields' => array( 'ID', 'user_registered' ), // Return only the ID and registered date. // 'role__in' => 'all', // @TODO: could be an array of roles, like ['customer', 'cashier'] ); @@ -262,36 +268,81 @@ public function wcpos_get_all_posts( array $fields = array() ): array { $last_updates = array(); if ( $id_with_modified_date ) { - $last_update_results = $wpdb->get_results( - " - SELECT user_id, meta_value - FROM $wpdb->usermeta - WHERE meta_key = 'last_update'", - OBJECT_K - ); - - foreach ( $last_update_results as $user_id => $meta ) { - $last_updates[ $user_id ] = $meta->meta_value; + $query = " + SELECT user_id, meta_value + FROM $wpdb->usermeta + WHERE meta_key = 'last_update' + "; + + // If modified_after param is set, add the condition to the query + if ( $modified_after ) { + $modified_after_timestamp = strtotime( $modified_after ); + $query .= $wpdb->prepare( ' AND meta_value > %d', $modified_after_timestamp ); + } + + $last_update_results = $wpdb->get_results( $query ); + + // Manually create the associative array of user_id => last_update + foreach ( $last_update_results as $result ) { + $last_updates[ $result->user_id ] = is_numeric( $result->meta_value ) ? gmdate( 'Y-m-d\TH:i:s', (int) $result->meta_value ) : null; } } - // Merge user IDs with their corresponding 'last_updated' values or fallback to user_registered. - $user_data = array_map( - function ( $user ) use ( $last_updates, $id_with_modified_date ) { - $user_info = array( 'id' => (int) $user->ID ); - if ( $id_with_modified_date ) { - if ( isset( $last_updates[ $user->ID ] ) ) { - $user_info['date_modified_gmt'] = wc_rest_prepare_date_response( $last_updates[ $user->ID ] ); - } else { - $user_info['date_modified_gmt'] = wc_rest_prepare_date_response( $user->user_registered ); + /** + * Performance notes: + * - Using a generator is faster than array_map when dealing with large datasets. + * - If date is in the format 'Y-m-d H:i:s' we just do preg_replace to 'Y-m-d\TH:i:s', + * rather than using wc_rest_prepare_date_response + * + * This resulted in execution time of 10% of the original time. + * + * If the modified_after param is set, we don't need to loop through the entire user list. + * The last_update_results array will only contain the users that have been modified after the given date. + * We just need to check they are valid user ids, this sucks, but there could be orphaned last_update meta values. + */ + $formatted_results = array(); + + if ( $modified_after ) { + foreach ( $users as $user ) { + if ( isset( $last_updates[ $user->ID ] ) ) { + $user_info = array( 'id' => (int) $user->ID ); + if ( $id_with_modified_date ) { + $user_info['date_modified_gmt'] = $last_updates[ $user->ID ]; } + $formatted_results[] = $user_info; } - return $user_info; - }, - $users - ); + } + } else { + function format_results( $users, $last_updates, $id_with_modified_date ) { + foreach ( $users as $user ) { + $user_info = array( 'id' => (int) $user->ID ); + if ( $id_with_modified_date ) { + if ( isset( $last_updates[ $user->ID ] ) && ! empty( $last_updates[ $user->ID ] ) ) { + $user_info['date_modified_gmt'] = $last_updates[ $user->ID ]; + } else { + $user_info['date_modified_gmt'] = null; // users can have null date_modified_gmt + } + } + yield $user_info; + } + } + + $formatted_results = iterator_to_array( format_results( $users, $last_updates, $id_with_modified_date ) ); + } + + // Get the total number of orders for the given criteria. + $total = count( $formatted_results ); + + // Collect execution time and server load. + $execution_time = microtime( true ) - $start_time; + $server_load = sys_getloadavg(); + + $response = rest_ensure_response( $formatted_results ); + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-Execution-Time', $execution_time ); + $response->header( 'X-Server-Load', json_encode( $server_load ) ); - return $user_data; + return $response; } catch ( Exception $e ) { Logger::log( 'Error fetching order IDs: ' . $e->getMessage() ); diff --git a/includes/API/Orders_Controller.php b/includes/API/Orders_Controller.php index 335d256..8dfa385 100644 --- a/includes/API/Orders_Controller.php +++ b/includes/API/Orders_Controller.php @@ -19,6 +19,7 @@ use WP_REST_Response; use WP_REST_Server; use Automattic\WooCommerce\Utilities\OrderUtil; +use WCPOS\WooCommercePOS\Services\Cache; use WP_Error; use const WCPOS\WooCommercePOS\PLUGIN_NAME; @@ -101,7 +102,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque * Optimised query for getting all order IDs. */ if ( $request->get_param( 'posts_per_page' ) == -1 && $request->get_param( 'fields' ) !== null ) { - return $this->wcpos_get_all_posts( $request->get_param( 'fields' ) ); + return $this->wcpos_get_all_posts( $request ); } return $dispatch_result; @@ -220,7 +221,7 @@ public function create_item( $request ) { Logger::log( 'UUID already in use, return existing order.', $ids[0] ); // Create a new WP_REST_Request object for the GET request. - $get_request = new WP_REST_Request( 'GET', '/wc/v3/orders/' . $ids[0] ); + $get_request = new WP_REST_Request( 'GET', $this->namespace . '/' . $this->rest_base . '/' . $ids[0] ); $get_request->set_param( 'id', $ids[0] ); return $this->get_item( $get_request ); @@ -669,13 +670,19 @@ public function wcpos_hpos_orders_table_query_clauses( array $clauses, $query, a /** * Returns array of all order ids. * - * @param array $fields Fields to return. + * @param WP_REST_Request $request Full details about the request. * - * @return array|WP_Error + * @return WP_REST_Response|WP_Error */ - public function wcpos_get_all_posts( array $fields = array() ): array { + public function wcpos_get_all_posts( $request ) { global $wpdb; + // Start timing execution. + $start_time = microtime( true ); + + $modified_after = $request->get_param( 'modified_after' ); + $dates_are_gmt = true; // Dates are always in GMT. + $fields = $request->get_param( 'fields' ); $id_with_modified_date = array( 'id', 'date_modified_gmt' ) === $fields; // Check if HPOS is enabled and custom orders table is used. @@ -694,6 +701,12 @@ function ( $status ) { $sql .= "SELECT DISTINCT {$select_fields} FROM {$wpdb->prefix}wc_orders WHERE type = 'shop_order'"; $sql .= ' AND status IN (' . implode( ',', $statuses ) . ')'; + // Add modified_after condition if provided. + if ( $modified_after ) { + $modified_after_date = date( 'Y-m-d H:i:s', strtotime( $modified_after ) ); + $sql .= $wpdb->prepare( ' AND date_updated_gmt > %s', $modified_after_date ); + } + // Order by date_created_gmt DESC to maintain order consistency. $sql .= " ORDER BY {$wpdb->prefix}wc_orders.date_created_gmt DESC"; } else { @@ -701,13 +714,33 @@ function ( $status ) { $sql .= "SELECT DISTINCT {$select_fields} FROM {$wpdb->posts} WHERE post_type = 'shop_order'"; $sql .= ' AND post_status IN (' . implode( ',', $statuses ) . ')'; + // Add modified_after condition if provided. + if ( $modified_after ) { + $modified_after_date = date( 'Y-m-d H:i:s', strtotime( $modified_after ) ); + $sql .= $wpdb->prepare( ' AND post_modified_gmt > %s', $modified_after_date ); + } + // Order by post_date DESC to maintain order consistency. $sql .= " ORDER BY {$wpdb->posts}.post_date DESC"; } try { - $results = $wpdb->get_results( $sql ); - return $this->wcpos_format_all_posts_response( $results ); + $results = $wpdb->get_results( $sql, ARRAY_A ); + $formatted_results = $this->wcpos_format_all_posts_response( $results ); + + // Get the total number of orders for the given criteria. + $total = count( $formatted_results ); + + // Collect execution time and server load. + $execution_time = microtime( true ) - $start_time; + $server_load = sys_getloadavg(); + + $response = rest_ensure_response( $formatted_results ); + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-Execution-Time', $execution_time ); + $response->header( 'X-Server-Load', json_encode( $server_load ) ); + + return $response; } catch ( Exception $e ) { Logger::log( 'Error fetching order data: ' . $e->getMessage() ); return new WP_Error( 'woocommerce_pos_rest_cannot_fetch', 'Error fetching order data.', array( 'status' => 500 ) ); diff --git a/includes/API/Product_Categories_Controller.php b/includes/API/Product_Categories_Controller.php index 52920a2..28653f9 100644 --- a/includes/API/Product_Categories_Controller.php +++ b/includes/API/Product_Categories_Controller.php @@ -56,7 +56,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque * Optimised query for getting all category IDs. */ if ( $request->get_param( 'posts_per_page' ) == -1 && $request->get_param( 'fields' ) !== null ) { - return $this->wcpos_get_all_posts( $request->get_param( 'fields' ) ); + return $this->wcpos_get_all_posts( $request ); } return $dispatch_result; @@ -141,11 +141,14 @@ public function wcpos_terms_clauses_include_exclude( array $clauses, array $taxo /** * Returns array of all product category ids. * - * @param array $fields + * @param WP_REST_Request $request Full details about the request. * - * @return array|WP_Error + * @return WP_REST_Response|WP_Error */ - public function wcpos_get_all_posts( array $fields = array() ): array { + public function wcpos_get_all_posts( $request ) { + // Start timing execution. + $start_time = microtime( true ); + $args = array( 'taxonomy' => 'product_cat', 'hide_empty' => false, @@ -156,12 +159,26 @@ public function wcpos_get_all_posts( array $fields = array() ): array { $results = get_terms( $args ); // Format the response. - return array_map( - function ( $item ) { - return array( 'id' => (int) $item ); + $formatted_results = array_map( + function ( $id ) { + return array( 'id' => (int) $id ); }, $results ); + + // Get the total number of orders for the given criteria. + $total = count( $formatted_results ); + + // Collect execution time and server load. + $execution_time = microtime( true ) - $start_time; + $server_load = sys_getloadavg(); + + $response = rest_ensure_response( $formatted_results ); + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-Execution-Time', $execution_time ); + $response->header( 'X-Server-Load', json_encode( $server_load ) ); + + return $response; } catch ( Exception $e ) { Logger::log( 'Error fetching product category IDs: ' . $e->getMessage() ); diff --git a/includes/API/Product_Tags_Controller.php b/includes/API/Product_Tags_Controller.php index e194e8a..99b1e74 100644 --- a/includes/API/Product_Tags_Controller.php +++ b/includes/API/Product_Tags_Controller.php @@ -56,7 +56,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque * Optimised query for getting all tag IDs. */ if ( $request->get_param( 'posts_per_page' ) == -1 && $request->get_param( 'fields' ) !== null ) { - return $this->wcpos_get_all_posts( $request->get_param( 'fields' ) ); + return $this->wcpos_get_all_posts( $request ); } return $dispatch_result; @@ -141,11 +141,14 @@ public function wcpos_terms_clauses_include_exclude( array $clauses, array $taxo /** * Returns array of all product tag ids. * - * @param array $fields + * @param WP_REST_Request $request Full details about the request. * - * @return array|WP_Error + * @return WP_REST_Response|WP_Error */ - public function wcpos_get_all_posts( array $fields = array() ): array { + public function wcpos_get_all_posts( $request ): array { + // Start timing execution. + $start_time = microtime( true ); + $args = array( 'taxonomy' => 'product_tag', 'hide_empty' => false, @@ -156,12 +159,26 @@ public function wcpos_get_all_posts( array $fields = array() ): array { $results = get_terms( $args ); // Format the response. - return array_map( - function ( $item ) { - return array( 'id' => (int) $item ); + $formatted_results = array_map( + function ( $id ) { + return array( 'id' => (int) $id ); }, $results ); + + // Get the total number of orders for the given criteria. + $total = count( $formatted_results ); + + // Collect execution time and server load. + $execution_time = microtime( true ) - $start_time; + $server_load = sys_getloadavg(); + + $response = rest_ensure_response( $formatted_results ); + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-Execution-Time', $execution_time ); + $response->header( 'X-Server-Load', json_encode( $server_load ) ); + + return $response; } catch ( Exception $e ) { Logger::log( 'Error fetching product tags IDs: ' . $e->getMessage() ); diff --git a/includes/API/Product_Variations_Controller.php b/includes/API/Product_Variations_Controller.php index db30f85..8d8c8ec 100644 --- a/includes/API/Product_Variations_Controller.php +++ b/includes/API/Product_Variations_Controller.php @@ -65,7 +65,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque * Optimised query for getting all product IDs. */ if ( $request->get_param( 'posts_per_page' ) == -1 && $request->get_param( 'fields' ) !== null ) { - return $this->wcpos_get_all_posts( $request->get_param( 'fields' ) ); + return $this->wcpos_get_all_posts( $request ); } return $dispatch_result; @@ -366,13 +366,17 @@ public function wcpos_posts_where_product_variation_include_exclude( string $whe /** * Returns array of all product ids, name. * - * @param array $fields + * @param WP_REST_Request $request Full details about the request. * - * @return array|WP_Error + * @return WP_REST_Response|WP_Error */ - public function wcpos_get_all_posts( array $fields = array() ): array { + public function wcpos_get_all_posts( $request ) { global $wpdb; + // Start timing execution. + $start_time = microtime( true ); + + $fields = $request->get_param( 'fields' ); $parent_id = (int) $this->wcpos_request->get_param( 'product_id' ); $id_with_modified_date = array( 'id', 'date_modified_gmt' ) === $fields; @@ -397,9 +401,22 @@ public function wcpos_get_all_posts( array $fields = array() ): array { try { // Execute the query. - $results = $wpdb->get_results( $sql ); - // Format and return the results. - return $this->wcpos_format_all_posts_response( $results ); + $results = $wpdb->get_results( $sql, ARRAY_A ); + $formatted_results = $this->wcpos_format_all_posts_response( $results ); + + // Get the total number of orders for the given criteria. + $total = count( $formatted_results ); + + // Collect execution time and server load. + $execution_time = microtime( true ) - $start_time; + $server_load = sys_getloadavg(); + + $response = rest_ensure_response( $formatted_results ); + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-Execution-Time', $execution_time ); + $response->header( 'X-Server-Load', json_encode( $server_load ) ); + + return $response; } catch ( Exception $e ) { Logger::log( 'Error fetching product variation IDs: ' . $e->getMessage() ); diff --git a/includes/API/Products_Controller.php b/includes/API/Products_Controller.php index 6d6886e..273ce10 100644 --- a/includes/API/Products_Controller.php +++ b/includes/API/Products_Controller.php @@ -75,7 +75,7 @@ public function wcpos_dispatch_request( $dispatch_result, WP_REST_Request $reque * Optimised query for getting all product IDs. */ if ( $request->get_param( 'posts_per_page' ) == -1 && $request->get_param( 'fields' ) !== null ) { - return $this->wcpos_get_all_posts( $request->get_param( 'fields' ) ); + return $this->wcpos_get_all_posts( $request ); } return $dispatch_result; @@ -429,13 +429,19 @@ public function wcpos_posts_where_product_include_exclude( string $where, WP_Que /** * Returns array of all product ids, name. * - * @param array $fields Fields to return. + * @param WP_REST_Request $request Full details about the request. * - * @return array|WP_Error + * @return WP_REST_Response|WP_Error */ - public function wcpos_get_all_posts( array $fields = array() ): array { + public function wcpos_get_all_posts( $request ) { global $wpdb; + // Start timing execution. + $start_time = microtime( true ); + + $modified_after = $request->get_param( 'modified_after' ); + $dates_are_gmt = true; // Dates are always in GMT. + $fields = $request->get_param( 'fields' ); $id_with_modified_date = array( 'id', 'date_modified_gmt' ) === $fields; $select_fields = $id_with_modified_date ? 'ID as id, post_modified_gmt as date_modified_gmt' : 'ID as id'; @@ -450,12 +456,32 @@ public function wcpos_get_all_posts( array $fields = array() ): array { $sql .= " WHERE post_type = 'product' AND post_status = 'publish'"; } + // Add modified_after condition if provided. + if ( $modified_after ) { + $modified_after_date = date( 'Y-m-d H:i:s', strtotime( $modified_after ) ); + $sql .= $wpdb->prepare( ' AND post_modified_gmt > %s', $modified_after_date ); + } + // Order by post_date DESC to maintain order consistency. $sql .= " ORDER BY {$wpdb->posts}.post_date DESC"; try { - $results = $wpdb->get_results( $sql ); - return $this->wcpos_format_all_posts_response( $results ); + $results = $wpdb->get_results( $sql, ARRAY_A ); + $formatted_results = $this->wcpos_format_all_posts_response( $results ); + + // Get the total number of orders for the given criteria. + $total = count( $formatted_results ); + + // Collect execution time and server load. + $execution_time = microtime( true ) - $start_time; + $server_load = sys_getloadavg(); + + $response = rest_ensure_response( $formatted_results ); + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-Execution-Time', $execution_time ); + $response->header( 'X-Server-Load', json_encode( $server_load ) ); + + return $response; } catch ( Exception $e ) { Logger::log( 'Error fetching product data: ' . $e->getMessage() ); return new WP_Error( 'woocommerce_pos_rest_cannot_fetch', 'Error fetching product data.', array( 'status' => 500 ) ); diff --git a/includes/API/Traits/WCPOS_REST_API.php b/includes/API/Traits/WCPOS_REST_API.php index 15ffbd9..6cb67ed 100644 --- a/includes/API/Traits/WCPOS_REST_API.php +++ b/includes/API/Traits/WCPOS_REST_API.php @@ -14,24 +14,30 @@ trait WCPOS_REST_API { * @return array An array of associative arrays with post information. */ public function wcpos_format_all_posts_response( $results ) { - $formatted_results = array_map( - function ( $result ) { - // Initialize the formatted result as an associative array. - $formatted_result = array( - 'id' => (int) $result->id, // Cast ID to integer for consistency. - ); + /** + * Performance notes: + * - Using a generator is faster than array_map when dealing with large datasets. + * - If date is in the format 'Y-m-d H:i:s' we just do preg_replace to 'Y-m-d\TH:i:s', rather than using wc_rest_prepare_date_response + * + * This resulted in execution time of 10% of the original time. + */ + function format_results( $results ) { + foreach ( $results as $result ) { + $result['id'] = (int) $result['id']; - // Check if post_modified_gmt exists and is not null, then set date_modified_gmt. - if ( isset( $result->date_modified_gmt ) && ! empty( $result->date_modified_gmt ) ) { - $formatted_result['date_modified_gmt'] = wc_rest_prepare_date_response( $result->date_modified_gmt ); + if ( isset( $result['date_modified_gmt'] ) ) { + if ( preg_match( '/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result['date_modified_gmt'] ) ) { + $result['date_modified_gmt'] = preg_replace( '/(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})/', '$1T$2', $result['date_modified_gmt'] ); + } else { + $result['date_modified_gmt'] = wc_rest_prepare_date_response( $result['date_modified_gmt'] ); + } } - return $formatted_result; - }, - $results - ); + yield $result; + } + } - return $formatted_results; + return iterator_to_array( format_results( $results ) ); } /** diff --git a/includes/Templates.php b/includes/Templates.php index 355f473..92b8631 100644 --- a/includes/Templates.php +++ b/includes/Templates.php @@ -149,8 +149,10 @@ private function load_template( string $classname ): void { * @return string */ public function order_received_url( string $order_received_url, WC_Abstract_Order $order ): string { + global $wp; + // check is pos - if ( ! woocommerce_pos_request() ) { + if ( ! woocommerce_pos_request() || ! isset( $wp->query_vars['order-pay'] ) ) { return $order_received_url; } diff --git a/includes/Templates/Login.php b/includes/Templates/Login.php index 9be1905..03be642 100644 --- a/includes/Templates/Login.php +++ b/includes/Templates/Login.php @@ -78,7 +78,6 @@ function ( $store ) { echo '