diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 135c9a421be..2cd21f8c163 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -69,7 +69,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control */ public function get_items( $request ) { $query_args = $this->prepare_reports_query( $request ); - $customers_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class. + $customers_query = new WC_Reports_Customers_Query( $query_args ); // @todo change to correct class. $report_data = $customers_query->get_data(); $out_data = array( 'totals' => get_object_vars( $report_data->totals ), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php new file mode 100644 index 00000000000..4dbdffc5c3b --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php @@ -0,0 +1,48 @@ + '2018-07-19 00:00:00', + * 'registered_after' => '2018-07-05 00:00:00', + * 'page' => 2, + * 'avg_order_value_min' => 100, + * 'country' => 'GB', + * ); + * $report = new WC_Admin_Reports_Customers_Query( $args ); + * $mydata = $report->get_data(); + * + * @package WooCommerce Admin/Classes + */ + +defined( 'ABSPATH' ) || exit; + +/** + * WC_Admin_Reports_Customers_Query + */ +class WC_Admin_Reports_Customers_Query extends WC_Admin_Reports_Query { + + /** + * Valid fields for Customers report. + * + * @return array + */ + protected function get_default_query_vars() { + return array(); + } + + /** + * Get product data based on the current query vars. + * + * @return array + */ + public function get_data() { + $args = apply_filters( 'woocommerce_reports_customers_query_args', $this->get_query_vars() ); + + $data_store = WC_Data_Store::load( 'report-customers' ); + $results = $data_store->get_data( $args ); + return apply_filters( 'woocommerce_reports_customers_select_query', $results, $args ); + } + +} diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php new file mode 100644 index 00000000000..80053cf4942 --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -0,0 +1,296 @@ + 'intval', + 'user_id' => 'intval', + 'date_registered' => 'strval', + 'date_last_active' => 'strval', + 'orders_count' => 'intval', + 'total_spend' => 'floatval', + 'avg_order_value' => 'floatval', + ); + + /** + * SQL columns to select in the db query and their mapping to SQL code. + * + * @var array + */ + protected $report_columns = array( + 'customer_id' => 'customer_id', + 'user_id' => 'user_id', + 'username' => 'username', + 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // TODO: what does this mean for RTL? + 'email' => 'email', + 'country' => 'country', + 'city' => 'city', + 'postcode' => 'postcode', + 'date_registered' => 'date_registered', + 'date_last_active' => 'date_last_active', + 'orders_count' => 'orders_count', + 'total_spend' => 'total_spend', + 'avg_order_value' => 'avg_order_value', + ); + + /** + * Maps ordering specified by the user to columns in the database/fields in the data. + * + * @param string $order_by Sorting criterion. + * @return string + */ + protected function normalize_order_by( $order_by ) { + if ( 'name' === $order_by ) { + return "CONCAT_WS( ' ', first_name, last_name )"; + } + + return $order_by; + } + + /** + * Fills ORDER BY clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_order_by_sql_params( $query_args ) { + $sql_query['order_by_clause'] = ''; + + if ( isset( $query_args['orderby'] ) ) { + $sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] ); + } + + if ( isset( $query_args['order'] ) ) { + $sql_query['order_by_clause'] .= ' ' . $query_args['order']; + } else { + $sql_query['order_by_clause'] .= ' DESC'; + } + + return $sql_query; + } + + /** + * Fills WHERE clause of SQL request with date-related constraints. + * + * @param array $query_args Parameters supplied by the user. + * @param string $table_name Name of the db table relevant for the date constraint. + * @return array + */ + protected function get_time_period_sql_params( $query_args, $table_name ) { + $sql_query = array( + 'from_clause' => '', + 'where_time_clause' => '', + 'where_clause' => '', + ); + $date_param_mapping = array( + 'registered' => 'date_registered', + 'last_active' => 'date_last_active', + 'last_order' => 'date_last_order', + ); + + // TODO: handle any/or match? + foreach ( $date_param_mapping as $query_param => $column_name ) { + $before_arg = $query_param . '_before'; + $after_arg = $query_param . '_after'; + + if ( ! empty( $query_args[ $before_arg ] ) ) { + $datetime = new DateTime( $query_args[ $before_arg ] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.{$column_name} <= '$datetime_str'"; + } + + if ( ! empty( $query_args[ $after_arg ] ) ) { + $datetime = new DateTime( $query_args[ $after_arg ] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.{$column_name} >= '$datetime_str'"; + } + } + + return $sql_query; + } + + /** + * Updates the database query with parameters used for Customers report: categories and order status. + * + * @param array $query_args Query arguments supplied by the user. + * @return array Array of parameters used for SQL query. + */ + protected function get_sql_query_params( $query_args ) { + global $wpdb; + $order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME; + + $sql_query_params = $this->get_time_period_sql_params( $query_args, $order_product_lookup_table ); + $sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) ); + $sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) ); + + $match_operator = $this->get_match_operator( $query_args ); + $where_clauses = array(); + + $exact_match_params = array( + 'name', + 'username', + 'email', + 'country', + ); + + foreach ( $exact_match_params as $exact_match_param ) { + if ( ! empty( $query_args[ $exact_match_param ] ) ) { + $where_clauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$exact_match_param} = %s", $query_args[ $exact_match_param ] ); + } + } + + $numeric_params = array( + 'orders_count' => '%d', + 'total_spend' => '%f', + 'avg_order_value' => '%f', + ); + + foreach ( $numeric_params as $numeric_param => $sql_format ) { + $subclauses = array(); + $min_param = $numeric_param . '_min'; + $max_param = $numeric_param . '_max'; + + if ( isset( $query_args[ $min_param ] ) ) { + $subclauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$numeric_param} >= {$sql_format}", $query_args[ $min_param ] ); + } + + if ( isset( $query_args[ $max_param ] ) ) { + $subclauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$numeric_param} <= {$sql_format}", $query_args[ $max_param ] ); + } + + if ( $subclauses ) { + $where_clauses[] = '(' . implode( " {$match_operator} ", $subclauses ) . ')'; + } + } + + if ( $where_clauses ) { + $sql_query_params['where_clause'] = implode( " {$match_operator} ", $where_clauses ); + } + + return $sql_query_params; + } + + /** + * Returns the report data based on parameters supplied by the user. + * + * @param array $query_args Query parameters. + * @return stdClass|WP_Error Data. + */ + public function get_data( $query_args ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + + // These defaults are only partially applied when used via REST API, as that has its own defaults. + $defaults = array( + 'per_page' => get_option( 'posts_per_page' ), + 'page' => 1, + 'order' => 'DESC', + 'orderby' => 'date_registered', + 'fields' => '*', + ); + $query_args = wp_parse_args( $query_args, $defaults ); + + $cache_key = $this->get_cache_key( $query_args ); + $data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false === $data ) { + $data = (object) array( + 'data' => array(), + 'total' => 0, + 'pages' => 0, + 'page_no' => 0, + ); + + $selections = $this->selected_columns( $query_args ); + $sql_query_params = $this->get_sql_query_params( $query_args ); + + $db_records_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM ( + SELECT + customer_id + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + customer_id + ) AS tt" + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] ); + if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { + return $data; + } + + $customer_data = $wpdb->get_results( + "SELECT + {$selections} + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + customer_id + ORDER BY + {$sql_query_params['order_by_clause']} + {$sql_query_params['limit']} + ", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + if ( null === $customer_data ) { + return $data; + } + + $customer_data = array_map( array( $this, 'cast_numbers' ), $customer_data ); + $data = (object) array( + 'data' => $customer_data, + 'total' => $db_records_count, + 'pages' => $total_pages, + 'page_no' => (int) $query_args['page'], + ); + + wp_cache_set( $cache_key, $data, $this->cache_group ); + } + + return $data; + } + + /** + * Returns string to be used as cache key for the data. + * + * @param array $params Query parameters. + * @return string + */ + protected function get_cache_key( $params ) { + return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); + } + +}