* Class for adding segmenting support without cluttering the data stores.
namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore as TaxesStatsDataStore;
use Automattic\WooCommerce\Enums\ProductType;
* Date & time interval and numeric range handling class for Reporting API.
* Array of all segment ids.
protected $all_segment_ids = false;
* Array of all segment labels.
protected $segment_labels = array();
* Query arguments supplied by the user for data store.
protected $query_args = '';
* SQL definition for each column.
protected $report_columns = array();
* @param array $query_args Query arguments supplied by the user for data store.
* @param array $report_columns Report columns lookup from data store.
public function __construct( $query_args, $report_columns ) {
$this->query_args = $query_args;
$this->report_columns = $report_columns;
* Filters definitions for SELECT clauses based on query_args and joins them into one string usable in SELECT clause.
* @param array $columns_mapping Column name -> SQL statememt mapping.
* @return string to be used in SELECT clause statements.
protected function prepare_selections( $columns_mapping ) {
if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) {
foreach ( $this->query_args['fields'] as $field ) {
if ( isset( $columns_mapping[ $field ] ) ) {
$keep[ $field ] = $columns_mapping[ $field ];
$selections = implode( ', ', $keep );
$selections = implode( ', ', $columns_mapping );
$selections = ',' . $selections;
* Update row-level db result for segments in 'totals' section to the format used for output.
* @param array $segments_db_result Results from the SQL db query for segmenting.
* @param string $segment_dimension Name of column used for grouping the result.
* @return array Reformatted array.
protected function reformat_totals_segments( $segments_db_result, $segment_dimension ) {
$segment_result = array();
if ( strpos( $segment_dimension, '.' ) ) {
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
$segment_labels = $this->get_segment_labels();
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
unset( $segment_data[ $segment_dimension ] );
'segment_id' => $segment_id,
'segment_label' => $segment_labels[ $segment_id ],
'subtotals' => $segment_data,
$segment_result[ $segment_id ] = $segment_datum;
* Merges segmented results for totals response part.
* 'avg_order_value' => 25,
* 'avg_order_value' => 25,
* @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets.
* @param array $result1 Array 1 of segmented figures.
* @param array $result2 Array 2 of segmented figures.
protected function merge_segment_totals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
$segment_labels = $this->get_segment_labels();
foreach ( $result1 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
unset( $segment_data[ $segment_dimension ] );
$result_segments[ $segment_id ] = array(
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => $segment_data,
foreach ( $result2 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
unset( $segment_data[ $segment_dimension ] );
if ( ! isset( $result_segments[ $segment_id ] ) ) {
$result_segments[ $segment_id ] = array(
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
$result_segments[ $segment_id ]['subtotals'] = array_merge( $result_segments[ $segment_id ]['subtotals'], $segment_data );
* Merges segmented results for intervals response part.
* 'time_interval' => '2018-12'
* 'time_interval' => '2018-12'
* 'avg_order_value' => 25,
* 'avg_order_value' => 25,
* @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets.
* @param array $result1 Array 1 of segmented figures.
* @param array $result2 Array 2 of segmented figures.
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
$segment_labels = $this->get_segment_labels();
foreach ( $result1 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $result_segments[ $time_interval ] ) ) {
$result_segments[ $time_interval ] = array();
$result_segments[ $time_interval ]['segments'] = array();
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
unset( $segment_data[ $segment_dimension ] );
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => $segment_data,
$result_segments[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
foreach ( $result2 as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $result_segments[ $time_interval ] ) ) {
$result_segments[ $time_interval ] = array();
$result_segments[ $time_interval ]['segments'] = array();
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
unset( $segment_data[ $segment_dimension ] );
if ( ! isset( $result_segments[ $time_interval ]['segments'][ $segment_id ] ) ) {
$result_segments[ $time_interval ]['segments'][ $segment_id ] = array(
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
$result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'] = array_merge( $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'], $segment_data );
* Update row-level db result for segments in 'intervals' section to the format used for output.
* @param array $segments_db_result Results from the SQL db query for segmenting.
* @param string $segment_dimension Name of column used for grouping the result.
* @return array Reformatted array.
protected function reformat_intervals_segments( $segments_db_result, $segment_dimension ) {
$aggregated_segment_result = array();
if ( strpos( $segment_dimension, '.' ) ) {
$segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 );
$segment_labels = $this->get_segment_labels();
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
if ( ! isset( $segment_labels[ $segment_id ] ) ) {
$time_interval = $segment_data['time_interval'];
if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) {
$aggregated_segment_result[ $time_interval ] = array();
$aggregated_segment_result[ $time_interval ]['segments'] = array();
unset( $segment_data['time_interval'] );
unset( $segment_data['datetime_anchor'] );
unset( $segment_data[ $segment_dimension ] );
'segment_label' => $segment_labels[ $segment_id ],
'segment_id' => $segment_id,
'subtotals' => $segment_data,
$aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum;
return $aggregated_segment_result;
* Fetches all segment ids from db and stores it for later use.
protected function set_all_segments() {
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
$this->all_segment_ids = array();
$segment_labels = array();
if ( 'product' === $this->query_args['segmentby'] ) {
if ( isset( $this->query_args['product_includes'] ) ) {
$args['include'] = $this->query_args['product_includes'];
if ( isset( $this->query_args['category_includes'] ) ) {
$categories = $this->query_args['category_includes'];
$args['category'] = array();
foreach ( $categories as $category_id ) {
$terms = get_term_by( 'id', $category_id, 'product_cat' );
$args['category'][] = $terms->slug;
$segment_objects = wc_get_products( $args );
foreach ( $segment_objects as $segment ) {
$id = $segment->get_id();
$segment_labels[ $id ] = $segment->get_name();
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
'type' => ProductType::VARIATION,
isset( $this->query_args['product_includes'] ) &&
is_array( $this->query_args['product_includes'] ) &&
count( $this->query_args['product_includes'] ) === 1
$args['parent'] = $this->query_args['product_includes'][0];
if ( isset( $this->query_args['variation_includes'] ) ) {
$args['include'] = $this->query_args['variation_includes'];
$segment_objects = wc_get_products( $args );
foreach ( $segment_objects as $segment ) {
$id = $segment->get_id();
$product_name = $segment->get_name();
$separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $segment );
$attributes = wc_get_formatted_variation( $segment, true, false );
$segment_labels[ $id ] = $product_name . $separator . $attributes;
// If no variations were specified, add a segment for the parent product (variation = 0).
// This is to catch simple products with prior sales converted into variable products.
// See: https://github.com/woocommerce/woocommerce-admin/issues/2719.
if ( isset( $args['parent'] ) && empty( $args['include'] ) ) {
$parent_object = wc_get_product( $args['parent'] );
$segment_labels[0] = $parent_object->get_name();
} elseif ( 'category' === $this->query_args['segmentby'] ) {
'taxonomy' => 'product_cat',
if ( isset( $this->query_args['category_includes'] ) ) {
$args['include'] = $this->query_args['category_includes'];
// @todo: Look into `wc_get_products` or data store methods and not directly touching the database or post types.
$categories = get_categories( $args );
$segments = wp_list_pluck( $categories, 'cat_ID' );
$segment_labels = wp_list_pluck( $categories, 'name', 'cat_ID' );
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
if ( isset( $this->query_args['coupons'] ) ) {
$args['include'] = $this->query_args['coupons'];
$coupons_store = new CouponsDataStore();
$coupons = $coupons_store->get_coupons( $args );
$segments = wp_list_pluck( $coupons, 'ID' );
$segment_labels = wp_list_pluck( $coupons, 'post_title', 'ID' );
$segment_labels = array_map( 'wc_format_coupon_code', $segment_labels );
} elseif ( 'customer_type' === $this->query_args['segmentby'] ) {
// 1 -- returning customer
$segments = array( 0, 1 );
} elseif ( 'tax_rate_id' === $this->query_args['segmentby'] ) {
if ( isset( $this->query_args['taxes'] ) ) {
$args['include'] = $this->query_args['taxes'];
$taxes = TaxesStatsDataStore::get_taxes( $args );
foreach ( $taxes as $tax ) {
$id = $tax['tax_rate_id'];
$segment_labels[ $id ] = \WC_Tax::get_rate_code( (object) $tax );
$this->all_segment_ids = $segments;
$this->segment_labels = $segment_labels;
* Return all segment ids for given segmentby query parameter.
protected function get_all_segments() {
if ( ! is_array( $this->all_segment_ids ) ) {
$this->set_all_segments();
return $this->all_segment_ids;
* Return all segment labels for given segmentby query parameter.
protected function get_segment_labels() {
if ( ! is_array( $this->all_segment_ids ) ) {
$this->set_all_segments();
return $this->segment_labels;
* Compares two report data objects by pre-defined object property and ASC/DESC ordering.
* @param stdClass $a Object a.
* @param stdClass $b Object b.
private function segment_cmp( $a, $b ) {
if ( $a['segment_id'] === $b['segment_id'] ) {
} elseif ( $a['segment_id'] > $b['segment_id'] ) {
} elseif ( $a['segment_id'] < $b['segment_id'] ) {
* Adds zeroes for segments not present in the data selection.
* @param array $segments Array of segments from the database for given data points.