From 9246ab6ef47050676ba3cb2e233edd41834241ec Mon Sep 17 00:00:00 2001 From: Allen Snook Date: Wed, 7 Nov 2018 07:25:17 -0500 Subject: [PATCH] Correct merge of class-wc-admin-reports-data-store --- .../class-wc-admin-reports-data-store.php | 584 ++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php new file mode 100644 index 00000000000..4d2eaf91140 --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php @@ -0,0 +1,584 @@ +order_by || '' === $this->order ) { + return 0; + // TODO: should return WP_Error here perhaps? + } + if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) { + return 0; + } elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) { + return strtolower( $this->order ) === 'desc' ? -1 : 1; + } elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) { + return strtolower( $this->order ) === 'desc' ? 1 : -1; + } + } + + /** + * Sorts intervals according to user's request. + * + * They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones. + * + * @param stdClass $data Data object, must contain an array under $data->intervals. + * @param string $sort_by Ordering property. + * @param string $direction DESC/ASC. + */ + protected function sort_intervals( &$data, $sort_by, $direction ) { + $this->order_by = $this->normalize_order_by( $sort_by ); + $this->order = $direction; + usort( $data->intervals, array( $this, 'interval_cmp' ) ); + } + + /** + * Fills in interval gaps from DB with 0-filled objects. + * + * @param array $db_intervals Array of all intervals present in the db. + * @param DateTime $datetime_start Start date. + * @param DateTime $datetime_end End date. + * @param string $time_interval Time interval, e.g. day, week, month. + * @param stdClass $data Data with SQL extracted intervals. + * @return stdClass + */ + protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) { + // TODO: this is ugly and messy. + // At this point, we don't know when we can stop iterating, as the ordering can be based on any value. + $end_datetime = new DateTime( $datetime_end ); + $time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) ); + $db_intervals = array_flip( $db_intervals ); + $datetime = new DateTime( $datetime_start ); + // Totals object used to get all needed properties. + $totals_arr = get_object_vars( $data->totals ); + foreach ( $totals_arr as $key => $val ) { + $totals_arr[ $key ] = 0; + } + while ( $datetime <= $end_datetime ) { + $next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); + $time_id = WC_Admin_Reports_Interval::time_interval_id( $time_interval, $datetime ); + // Either create fill-zero interval or use data from db. + if ( $next_start > $end_datetime ) { + $interval_end = $end_datetime->format( 'Y-m-d H:i:s' ); + } else { + $prev_end_timestamp = (int) $next_start->format( 'U' ) - 1; + $prev_end = new DateTime(); + $prev_end->setTimestamp( $prev_end_timestamp ); + $interval_end = $prev_end->format( 'Y-m-d H:i:s' ); + } + if ( array_key_exists( $time_id, $time_ids ) ) { + // For interval present in the db for this time frame, just fill in dates. + $record = &$data->intervals[ $time_ids[ $time_id ] ]; + $record['date_start'] = $datetime->format( 'Y-m-d H:i:s' ); + $record['date_end'] = $interval_end; + } elseif ( ! array_key_exists( $time_id, $db_intervals ) ) { + // For intervals present in the db outside of this time frame, do nothing. + // For intervals not present in the db, fabricate it. + $record_arr = array(); + $record_arr['time_interval'] = $time_id; + $record_arr['date_start'] = $datetime->format( 'Y-m-d H:i:s' ); + $record_arr['date_end'] = $interval_end; + $data->intervals[] = array_merge( $record_arr, $totals_arr ); + } + $datetime = $next_start; + } + return $data; + } + + /** + * Removes extra records from intervals so that only requested number of records get returned. + * + * @param stdClass $data Data from whose intervals the records get removed. + * @param int $page_no Offset requested by the user. + * @param int $items_per_page Number of records requested by the user. + * @param int $db_interval_count Database interval count. + * @param int $expected_interval_count Expected interval count on the output. + * @param string $order_by Order by field. + */ + protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by ) { + if ( 'date' === strtolower( $order_by ) ) { + $offset = 0; + } else { + $offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count; + $offset = $offset < 0 ? 0 : $offset; + } + $count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page; + if ( $count < 0 ) { + $count = 0; + } elseif ( $count > $items_per_page ) { + $count = $items_per_page; + } + $data->intervals = array_slice( $data->intervals, $offset, $count ); + } + + /** + * Updates the LIMIT query part for Intervals query of the report. + * + * If there are less records in the database than time intervals, then we need to remap offset in SQL query + * to fetch correct records. + * + * @param array $intervals_query Array with clauses for the Intervals SQL query. + * @param array $query_args Query arguements. + * @param int $db_interval_count Database interval count. + * @param int $expected_interval_count Expected interval count on the output. + */ + protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count ) { + if ( $db_interval_count === $expected_interval_count ) { + return; + } + if ( 'date' === strtolower( $query_args['orderby'] ) ) { + // page X in request translates to slightly different dates in the db, in case some + // records are missing from the db. + $start_iteration = 0; + $end_iteration = 0; + if ( 'asc' === strtolower( $query_args['order'] ) ) { + // ORDER BY date ASC. + $new_start_date = new DateTime( $query_args['after'] ); + $intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page']; + $latest_end_date = new DateTime( $query_args['before'] ); + for ( $i = 0; $i < $intervals_to_skip; $i++ ) { + if ( $new_start_date > $latest_end_date ) { + $new_start_date = $latest_end_date; + $start_iteration = 0; + break; + } + $new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'] ); + $start_iteration ++; + } + + $new_end_date = clone $new_start_date; + for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) { + if ( $new_end_date > $latest_end_date ) { + $new_end_date = $latest_end_date; + $end_iteration = 0; + break; + } + $new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'] ); + $end_iteration ++; + } + if ( $end_iteration ) { + $new_end_date_timestamp = (int) $new_end_date->format( 'U' ) - 1; + $new_end_date->setTimestamp( $new_end_date_timestamp ); + } + } else { + // ORDER BY date DESC. + $new_end_date = new DateTime( $query_args['before'] ); + $intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page']; + $earliest_start_date = new DateTime( $query_args['after'] ); + for ( $i = 0; $i < $intervals_to_skip; $i++ ) { + if ( $new_end_date < $earliest_start_date ) { + $new_end_date = $earliest_start_date; + $end_iteration = 0; + break; + } + $new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'], true ); + $end_iteration ++; + } + + $new_start_date = clone $new_end_date; + for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) { + if ( $new_start_date < $earliest_start_date ) { + $new_start_date = $earliest_start_date; + $start_iteration = 0; + break; + } + $new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'], true ); + $start_iteration ++; + } + if ( $start_iteration ) { + // TODO: is this correct? should it only be added if iterate runs? other two iterate instances, too? + $new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1; + $new_start_date->setTimestamp( $new_start_date_timestamp ); + } + } + $query_args['adj_after'] = $new_start_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); + $query_args['adj_before'] = $new_end_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); + $intervals_query['where_clause'] = ''; + $intervals_query['where_clause'] .= " AND date_created <= '{$query_args['adj_before']}'"; + $intervals_query['where_clause'] .= " AND date_created >= '{$query_args['adj_after']}'"; + $intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page']; + } else { + if ( 'asc' === $query_args['order'] ) { + $offset = ( ( $query_args['page'] - 1 ) * $intervals_query['per_page'] ) - ( $expected_interval_count - $db_interval_count ); + $offset = $offset < 0 ? 0 : $offset; + $count = $query_args['page'] * $intervals_query['per_page'] - ( $expected_interval_count - $db_interval_count ); + if ( $count < 0 ) { + $count = 0; + } elseif ( $count > $intervals_query['per_page'] ) { + $count = $intervals_query['per_page']; + } + $intervals_query['limit'] = 'LIMIT ' . $offset . ',' . $count; + } + // Otherwise no change in limit clause. + $query_args['adj_after'] = $query_args['after']; + $query_args['adj_before'] = $query_args['before']; + } + } + + /** + * Returns string to be used as cache key for the data. + * + * @param array $params Query parameters. + * @return string + */ + protected function get_cache_key( $params ) { + // TODO: this is not working in PHP 5.2 (but revenue class has static methods, so it cannot use object property). + return 'woocommerce_' . $this::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); // phpcs:ignore PHPCompatibility.Syntax.NewDynamicAccessToStatic + } + + /** + * Casts strings returned from the database to appropriate data types for output. + * + * @param array $array Associative array of values extracted from the database. + * @return array|WP_Error + */ + protected function cast_numbers( $array ) { + $retyped_array = array(); + $column_types = apply_filters( 'woocommerce_rest_reports_column_types', $this->column_types, $array ); + foreach ( $array as $column_name => $value ) { + if ( isset( $column_types[ $column_name ] ) ) { + $retyped_array[ $column_name ] = $column_types[ $column_name ]( $value ); + } else { + $retyped_array[ $column_name ] = $value; + } + } + return $retyped_array; + } + + /** + * Returns a list of columns selected by the query_args formatted as a comma separated string. + * + * @param array $query_args User-supplied options. + * @return string + */ + protected function selected_columns( $query_args ) { + $selections = $this->report_columns; + + if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { + $keep = array(); + foreach ( $query_args['fields'] as $field ) { + if ( isset( $selections[ $field ] ) ) { + $keep[ $field ] = $selections[ $field ]; + } + } + $selections = implode( ', ', $keep ); + } else { + $selections = implode( ', ', $selections ); + } + return $selections; + } + + /** + * Get the order statuses used when calculating reports. + * + * @return array + */ + protected static function get_report_order_statuses() { + return apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ); + } + + /** + * Maps order status provided by the user to the one used in the database. + * + * @param string $status Order status. + * @return string + */ + protected function normalize_order_status( $status ) { + $status = trim( $status ); + return 'wc-' . $status; + } + + /** + * Normalizes order_by clause to match to SQL query. + * + * @param string $order_by Order by option requeste by user. + * @return string + */ + protected function normalize_order_by( $order_by ) { + if ( 'date' === $order_by ) { + return 'time_interval'; + } + + return $order_by; + } + + /** + * Updates start and end dates for intervals so that they represent intervals' borders, not times when data in db were recorded. + * + * E.g. if there are db records for only Tuesday and Thursday this week, the actual week interval is [Mon, Sun], not [Tue, Thu]. + * + * @param DateTime $datetime_start Start date. + * @param DateTime $datetime_end End date. + * @param string $time_interval Time interval, e.g. day, week, month. + * @param array $intervals Array of intervals extracted from SQL db. + */ + protected function update_interval_boundary_dates( $datetime_start, $datetime_end, $time_interval, &$intervals ) { + foreach ( $intervals as $key => $interval ) { + $datetime = new DateTime( $interval['datetime_anchor'] ); + + $prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true ); + // TODO: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below. + $prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1; + $prev_start->setTimestamp( $prev_start_timestamp ); + if ( $datetime_start ) { + $start_datetime = new DateTime( $datetime_start ); + $date_start = $prev_start < $start_datetime ? $start_datetime : $prev_start; + $intervals[ $key ]['date_start'] = $date_start->format( 'Y-m-d H:i:s' ); + } else { + $intervals[ $key ]['date_start'] = $prev_start->format( 'Y-m-d H:i:s' ); + } + + $next_end = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); + $next_end_timestamp = (int) $next_end->format( 'U' ) - 1; + $next_end->setTimestamp( $next_end_timestamp ); + if ( $datetime_end ) { + $end_datetime = new DateTime( $datetime_end ); + $date_end = $next_end > $end_datetime ? $end_datetime : $next_end; + $intervals[ $key ]['date_end'] = $date_end->format( 'Y-m-d H:i:s' ); + } else { + $intervals[ $key ]['date_end'] = $next_end->format( 'Y-m-d H:i:s' ); + } + + $intervals[ $key ]['interval'] = $time_interval; + } + } + + /** + * Change structure of intervals to form a correct response. + * + * @param array $intervals Time interval, e.g. day, week, month. + */ + protected function create_interval_subtotals( &$intervals ) { + foreach ( $intervals as $key => $interval ) { + // Move intervals result to subtotals object. + $intervals[ $key ] = array( + 'interval' => $interval['time_interval'], + 'date_start' => $interval['date_start'], + 'date_start_gmt' => $interval['date_start'], + 'date_end' => $interval['date_end'], + 'date_end_gmt' => $interval['date_end'], + ); + + unset( $interval['interval'] ); + unset( $interval['date_start'] ); + unset( $interval['date_end'] ); + unset( $interval['datetime_anchor'] ); + unset( $interval['time_interval'] ); + $intervals[ $key ]['subtotals'] = (object) $this->cast_numbers( $interval ); + } + } + + /** + * Fills WHERE clause of SQL request for 'Totals' section of data response based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_time_period_sql_params( $query_args ) { + $sql_query = array( + 'from_clause' => '', + 'where_clause' => '', + ); + + if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) { + $datetime = new DateTime( $query_args['before'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_clause'] .= " AND date_created <= '$datetime_str'"; + + } + + if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) { + $datetime = new DateTime( $query_args['after'] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_clause'] .= " AND date_created >= '$datetime_str'"; + } + + return $sql_query; + } + + /** + * Fills LIMIT clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_limit_sql_params( $query_args ) { + $sql_query['per_page'] = get_option( 'posts_per_page' ); + if ( isset( $query_args['per_page'] ) && is_numeric( $query_args['per_page'] ) ) { + $sql_query['per_page'] = (int) $query_args['per_page']; + } + + $sql_query['offset'] = 0; + if ( isset( $query_args['page'] ) ) { + $sql_query['offset'] = ( (int) $query_args['page'] - 1 ) * $sql_query['per_page']; + } + + $sql_query['limit'] = "LIMIT {$sql_query['offset']}, {$sql_query['per_page']}"; + return $sql_query; + } + + /** + * 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 FROM and WHERE clauses of SQL request for 'Intervals' section of data response based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_intervals_sql_params( $query_args ) { + $intervals_query = array( + 'from_clause' => '', + 'where_clause' => '', + ); + + $intervals_query = array_merge( $intervals_query, $this->get_time_period_sql_params( $query_args ) ); + + if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) { + $interval = $query_args['interval']; + $intervals_query['select_clause'] = WC_Admin_Reports_Interval::db_datetime_format( $interval ); + } + + $intervals_query = array_merge( $intervals_query, $this->get_limit_sql_params( $query_args ) ); + + $intervals_query = array_merge( $intervals_query, $this->get_order_by_sql_params( $query_args ) ); + + return $intervals_query; + } + + /** + * Returns an array of products belonging to given categories. + * + * @param array $categories List of categories IDs. + * @return array|stdClass + */ + protected function get_products_by_cat_ids( $categories ) { + $product_categories = get_categories( + array( + 'hide_empty' => 0, + 'taxonomy' => 'product_cat', + ) + ); + $cat_slugs = array(); + $categories = array_flip( $categories ); + foreach ( $product_categories as $product_cat ) { + if ( key_exists( $product_cat->cat_ID, $categories ) ) { + $cat_slugs[] = $product_cat->slug; + } + } + $args = array( + 'category' => $cat_slugs, + 'limit' => -1, + ); + return wc_get_products( $args ); + } + + /** + * Returns ids of allowed products, based on query arguments from the user. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_allowed_products( $query_args ) { + $allowed_products = array(); + if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) { + $allowed_products = $this->get_products_by_cat_ids( $query_args['categories'] ); + $allowed_products = wc_list_pluck( $allowed_products, 'get_id' ); + } + + if ( isset( $query_args['products'] ) && is_array( $query_args['products'] ) && count( $query_args['products'] ) > 0 ) { + if ( count( $allowed_products ) > 0 ) { + $allowed_products = array_intersect( $allowed_products, $query_args['products'] ); + } else { + $allowed_products = $query_args['products']; + } + } + return $allowed_products; + } + +}