File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Variations.tar
Controller.php 0000644 00000035153 15155233514 0007413 0 ustar 00 <?php
/**
* REST API Reports products controller
*
* Handles requests to the /reports/products endpoint.
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
/**
* REST API Reports products controller class.
*
* @internal
* @extends ReportsController
*/
class Controller extends ReportsController implements ExportableInterface {
/**
* Exportable traits.
*/
use ExportableTraits;
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/variations';
/**
* Mapping between external parameter name and name used in query class.
*
* @var array
*/
protected $param_mapping = array(
'variations' => 'variation_includes',
);
/**
* Get items.
*
* @param WP_REST_Request $request Request data.
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$args = array();
/**
* Experimental: Filter the list of parameters provided when querying data from the data store.
*
* @ignore
*
* @param array $collection_params List of parameters.
*/
$collection_params = apply_filters(
'experimental_woocommerce_analytics_variations_collection_params',
$this->get_collection_params()
);
$registered = array_keys( $collection_params );
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
if ( isset( $this->param_mapping[ $param_name ] ) ) {
$args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
} else {
$args[ $param_name ] = $request[ $param_name ];
}
}
}
$reports = new Query( $args );
$products_data = $reports->get_data();
$data = array();
foreach ( $products_data->data as $product_data ) {
$item = $this->prepare_item_for_response( $product_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$data,
(int) $products_data->total,
(int) $products_data->page_no,
(int) $products_data->pages
);
}
/**
* Prepare a report object for serialization.
*
* @param array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_variations', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param array $object Object data.
* @return array Links for the given post.
*/
protected function prepare_links( $object ) {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
),
'variation' => array(
'href' => rest_url( sprintf( '/%s/%s/%d/%s/%d', $this->namespace, 'products', $object['product_id'], 'variation', $object['variation_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_varitations',
'type' => 'object',
'properties' => array(
'product_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product ID.', 'woocommerce' ),
),
'variation_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product ID.', 'woocommerce' ),
),
'items_sold' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Number of items sold.', 'woocommerce' ),
),
'net_revenue' => array(
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Total Net sales of all items sold.', 'woocommerce' ),
),
'orders_count' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Number of orders product appeared in.', 'woocommerce' ),
),
'extended_info' => array(
'name' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product name.', 'woocommerce' ),
),
'price' => array(
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product price.', 'woocommerce' ),
),
'image' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product image.', 'woocommerce' ),
),
'permalink' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product link.', 'woocommerce' ),
),
'attributes' => array(
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product attributes.', 'woocommerce' ),
),
'stock_status' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product inventory status.', 'woocommerce' ),
),
'stock_quantity' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product inventory quantity.', 'woocommerce' ),
),
'low_stock_amount' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product inventory threshold for low stock.', 'woocommerce' ),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'net_revenue',
'orders_count',
'items_sold',
'sku',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['variations'] = array(
'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each variation to the report.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_is'] = array(
'description' => __( 'Limit result set to variations that include the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'array',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_is_not'] = array(
'description' => __( 'Limit result set to variations that don\'t include the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'array',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
$params['category_includes'] = array(
'description' => __( 'Limit result set to variations in the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['category_excludes'] = array(
'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['force_cache_refresh'] = array(
'description' => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'wp_validate_boolean',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get stock status column export value.
*
* @param array $status Stock status from report row.
* @return string
*/
protected function get_stock_status( $status ) {
$statuses = wc_get_product_stock_status_options();
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
}
/**
* Get the column names for export.
*
* @return array Key value pair of Column ID => Label.
*/
public function get_export_columns() {
$export_columns = array(
'product_name' => __( 'Product / Variation title', 'woocommerce' ),
'sku' => __( 'SKU', 'woocommerce' ),
'items_sold' => __( 'Items sold', 'woocommerce' ),
'net_revenue' => __( 'N. Revenue', 'woocommerce' ),
'orders_count' => __( 'Orders', 'woocommerce' ),
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$export_columns['stock_status'] = __( 'Status', 'woocommerce' );
$export_columns['stock'] = __( 'Stock', 'woocommerce' );
}
return $export_columns;
}
/**
* Get the column values for export.
*
* @param array $item Single report item/row.
* @return array Key value pair of Column ID => Row Value.
*/
public function prepare_item_for_export( $item ) {
$export_item = array(
'product_name' => $item['extended_info']['name'],
'sku' => $item['extended_info']['sku'],
'items_sold' => $item['items_sold'],
'net_revenue' => self::csv_number_format( $item['net_revenue'] ),
'orders_count' => $item['orders_count'],
);
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$export_item['stock_status'] = $this->get_stock_status( $item['extended_info']['stock_status'] );
$export_item['stock'] = $item['extended_info']['stock_quantity'];
}
return $export_item;
}
}
DataStore.php 0000644 00000042711 15155233514 0007154 0 ustar 00 <?php
/**
* API\Reports\Variations\DataStore class file.
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Variations\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @var string
*/
protected static $table_name = 'wc_order_product_lookup';
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'variations';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'date_start' => 'strval',
'date_end' => 'strval',
'product_id' => 'intval',
'variation_id' => 'intval',
'items_sold' => 'intval',
'net_revenue' => 'floatval',
'orders_count' => 'intval',
'name' => 'strval',
'price' => 'floatval',
'image' => 'strval',
'permalink' => 'strval',
'sku' => 'strval',
);
/**
* Extended product attributes to include in the data.
*
* @var array
*/
protected $extended_attributes = array(
'name',
'price',
'image',
'permalink',
'stock_status',
'stock_quantity',
'low_stock_amount',
'sku',
);
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'variations';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'product_id' => 'product_id',
'variation_id' => 'variation_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT(DISTINCT {$table_name}.order_id) as orders_count",
);
}
/**
* Fills FROM clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @param string $arg_name Target of the JOIN sql param.
*/
protected function add_from_sql_params( $query_args, $arg_name ) {
global $wpdb;
if ( 'sku' !== $query_args['orderby'] ) {
return;
}
$table_name = self::get_db_table_name();
$join = "LEFT JOIN {$wpdb->postmeta} AS postmeta ON {$table_name}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'";
if ( 'inner' === $arg_name ) {
$this->subquery->add_sql_clause( 'join', $join );
} else {
$this->add_sql_clause( 'join', $join );
}
}
/**
* Generate a subquery for order_item_id based on the attribute filters.
*
* @param array $query_args Query arguments supplied by the user.
* @return string
*/
protected function get_order_item_by_attribute_subquery( $query_args ) {
$order_product_lookup_table = self::get_db_table_name();
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
// Perform a subquery for DISTINCT order items that match our attribute filters.
$attr_subquery = new SqlQuery( $this->context . '_attribute_subquery' );
$attr_subquery->add_sql_clause( 'select', "DISTINCT {$order_product_lookup_table}.order_item_id" );
$attr_subquery->add_sql_clause( 'from', $order_product_lookup_table );
if ( $this->should_exclude_simple_products( $query_args ) ) {
$attr_subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
}
foreach ( $attribute_subqueries['join'] as $attribute_join ) {
$attr_subquery->add_sql_clause( 'join', $attribute_join );
}
$operator = $this->get_match_operator( $query_args );
$attr_subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $attribute_subqueries['where'] ) . ')' );
return "AND {$order_product_lookup_table}.order_item_id IN ({$attr_subquery->get_query_statement()})";
}
return false;
}
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function add_sql_query_params( $query_args ) {
global $wpdb;
$order_product_lookup_table = self::get_db_table_name();
$order_stats_lookup_table = $wpdb->prefix . 'wc_order_stats';
$order_item_meta_table = $wpdb->prefix . 'woocommerce_order_itemmeta';
$where_subquery = array();
$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->get_limit_sql_params( $query_args );
$this->add_order_by_sql_params( $query_args );
$included_variations = $this->get_included_variations( $query_args );
if ( $included_variations > 0 ) {
$this->add_from_sql_params( $query_args, 'outer' );
} else {
$this->add_from_sql_params( $query_args, 'inner' );
}
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
}
$excluded_products = $this->get_excluded_products( $query_args );
if ( $excluded_products ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})" );
}
if ( $included_variations ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
} elseif ( ! $included_products ) {
if ( $this->should_exclude_simple_products( $query_args ) ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
}
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$order_stats_lookup_table} ON {$order_product_lookup_table}.order_id = {$order_stats_lookup_table}.order_id" );
$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
}
$attribute_order_items_subquery = $this->get_order_item_by_attribute_subquery( $query_args );
if ( $attribute_order_items_subquery ) {
// JOIN on product lookup if we haven't already.
if ( ! $order_status_filter ) {
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
}
// Add subquery for matching attributes to WHERE.
$this->subquery->add_sql_clause( 'where', $attribute_order_items_subquery );
}
if ( 0 < count( $where_subquery ) ) {
$operator = $this->get_match_operator( $query_args );
$this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' );
}
}
/**
* 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 ( 'date' === $order_by ) {
return self::get_db_table_name() . '.date_created';
}
if ( 'sku' === $order_by ) {
return 'meta_value';
}
return $order_by;
}
/**
* Enriches the product data with attributes specified by the extended_attributes.
*
* @param array $products_data Product data.
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$products_data, $query_args ) {
foreach ( $products_data as $key => $product_data ) {
$extended_info = new \ArrayObject();
if ( $query_args['extended_info'] ) {
$extended_attributes = apply_filters( 'woocommerce_rest_reports_variations_extended_attributes', $this->extended_attributes, $product_data );
$parent_product = wc_get_product( $product_data['product_id'] );
$attributes = array();
// Base extended info off the parent variable product if the variation ID is 0.
// This is caused by simple products with prior sales being converted into variable products.
// See: https://github.com/woocommerce/woocommerce-admin/issues/2719.
$variation_id = (int) $product_data['variation_id'];
$variation_product = ( 0 === $variation_id ) ? $parent_product : wc_get_product( $variation_id );
// Fall back to the parent product if the variation can't be found.
$extended_attributes_product = is_a( $variation_product, 'WC_Product' ) ? $variation_product : $parent_product;
// If both product and variation is not found, set deleted to true.
if ( ! $extended_attributes_product ) {
$extended_info['deleted'] = true;
}
foreach ( $extended_attributes as $extended_attribute ) {
$function = 'get_' . $extended_attribute;
if ( is_callable( array( $extended_attributes_product, $function ) ) ) {
$value = $extended_attributes_product->{$function}();
$extended_info[ $extended_attribute ] = $value;
}
}
// If this is a variation, add its attributes.
// NOTE: We don't fall back to the parent product here because it will include all possible attribute options.
if (
0 < $variation_id &&
is_callable( array( $variation_product, 'get_variation_attributes' ) )
) {
$variation_attributes = $variation_product->get_variation_attributes();
foreach ( $variation_attributes as $attribute_name => $attribute ) {
$name = str_replace( 'attribute_', '', $attribute_name );
$option_term = get_term_by( 'slug', $attribute, $name );
$attributes[] = array(
'id' => wc_attribute_taxonomy_id_by_name( $name ),
'name' => str_replace( 'pa_', '', $name ),
'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
);
}
}
$extended_info['attributes'] = $attributes;
// If there is no set low_stock_amount, use the one in user settings.
if ( '' === $extended_info['low_stock_amount'] ) {
$extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
}
$extended_info = $this->cast_numbers( $extended_info );
}
$products_data[ $key ]['extended_info'] = $extended_info;
}
}
/**
* Returns if simple products should be excluded from the report.
*
* @internal
*
* @param array $query_args Query parameters.
*
* @return boolean
*/
protected function should_exclude_simple_products( array $query_args ) {
return apply_filters( 'experimental_woocommerce_analytics_variations_should_exclude_simple_products', true, $query_args );
}
/**
* Fill missing extended_info.name for the deleted products.
*
* @param array $products Product data.
*/
protected function fill_deleted_product_name( array &$products ) {
global $wpdb;
$product_variation_ids = [];
// Find products with missing extended_info.name.
foreach ( $products as $key => $product ) {
if ( ! isset( $product['extended_info']['name'] ) ) {
$product_variation_ids[ $key ] = [
'product_id' => $product['product_id'],
'variation_id' => $product['variation_id'],
];
}
}
if ( ! count( $product_variation_ids ) ) {
return;
}
$where_clauses = implode(
' or ',
array_map(
function( $ids ) {
return "(
product_lookup.product_id = {$ids['product_id']}
and
product_lookup.variation_id = {$ids['variation_id']}
)";
},
$product_variation_ids
)
);
$query = "
select
product_lookup.product_id,
product_lookup.variation_id,
order_items.order_item_name
from
{$wpdb->prefix}wc_order_product_lookup as product_lookup
left join {$wpdb->prefix}woocommerce_order_items as order_items
on product_lookup.order_item_id = order_items.order_item_id
where
{$where_clauses}
group by
product_lookup.product_id,
product_lookup.variation_id,
order_items.order_item_name
";
// phpcs:ignore
$results = $wpdb->get_results( $query );
$index = [];
foreach ( $results as $result ) {
$index[ $result->product_id . '_' . $result->variation_id ] = $result->order_item_name;
}
foreach ( $product_variation_ids as $product_key => $ids ) {
$product = $products[ $product_key ];
$index_key = $product['product_id'] . '_' . $product['variation_id'];
if ( isset( $index[ $index_key ] ) ) {
$products[ $product_key ]['extended_info']['name'] = $index[ $index_key ];
}
}
}
/**
* 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 = self::get_db_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',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'product_includes' => array(),
'variation_includes' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$included_variations =
( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) )
? $query_args['variation_includes']
: array();
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
if ( count( $included_variations ) > 0 ) {
$total_results = count( $included_variations );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
if ( 'date' === $query_args['orderby'] ) {
$this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
}
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'variation_id' ) );
$ids_table = $this->get_ids_table( $included_variations, 'variation_id' );
$this->add_sql_clause( 'select', $join_selections );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.variation_id = {$table_name}.variation_id"
);
$variations_query = $this->get_query_statement();
} else {
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
/**
* Experimental: Filter the Variations SQL query allowing extensions to add additional SQL clauses.
*
* @since 7.4.0
* @param array $query_args Query parameters.
* @param SqlQuery $subquery Variations query class.
*/
apply_filters( 'experimental_woocommerce_analytics_variations_additional_clauses', $query_args, $this->subquery );
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
);
/* phpcs:enable */
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$variations_query = $this->subquery->get_query_statement();
}
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$product_data = $wpdb->get_results(
$variations_query,
ARRAY_A
);
/* phpcs:enable */
if ( null === $product_data ) {
return $data;
}
$this->include_extended_info( $product_data, $query_args );
if ( $query_args['extended_info'] ) {
$this->fill_deleted_product_name( $product_data );
}
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
$data = (object) array(
'data' => $product_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
$this->subquery = new SqlQuery( $this->context . '_subquery' );
$this->subquery->add_sql_clause( 'select', 'product_id' );
$this->subquery->add_sql_clause( 'from', self::get_db_table_name() );
$this->subquery->add_sql_clause( 'group_by', 'product_id, variation_id' );
}
}
Query.php 0000644 00000002366 15155233514 0006375 0 ustar 00 <?php
/**
* Class for parameter-based Products Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'categories' => array(15, 18),
* 'products' => array(1,2,3)
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Variations\Query( $args );
* $mydata = $report->get_data();
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Variations\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Products 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_analytics_variations_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-variations' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_variations_select_query', $results, $args );
}
}
Stats/Controller.php 0000644 00000023765 15155233514 0010517 0 ustar 00 <?php
/**
* REST API Reports variations stats controller
*
* Handles requests to the /reports/variations/stats endpoint.
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request;
use WP_REST_Response;
/**
* REST API Reports variations stats controller class.
*
* @internal
* @extends GenericStatsController
*/
class Controller extends GenericStatsController {
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/variations/stats';
/**
* Mapping between external parameter name and name used in query class.
*
* @var array
*/
protected $param_mapping = array(
'variations' => 'variation_includes',
);
/**
* Constructor.
*/
public function __construct() {
add_filter( 'woocommerce_analytics_variations_stats_select_query', array( $this, 'set_default_report_data' ) );
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = array(
'fields' => array(
'items_sold',
'net_revenue',
'orders_count',
'variations_count',
),
);
/**
* Experimental: Filter the list of parameters provided when querying data from the data store.
*
* @ignore
*
* @param array $collection_params List of parameters.
*/
$collection_params = apply_filters( 'experimental_woocommerce_analytics_variations_stats_collection_params', $this->get_collection_params() );
$registered = array_keys( $collection_params );
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
if ( isset( $this->param_mapping[ $param_name ] ) ) {
$query_args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
} else {
$query_args[ $param_name ] = $request[ $param_name ];
}
}
}
$query = new Query( $query_args );
try {
$report_data = $query->get_data();
} catch ( ParameterException $e ) {
return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$out_data,
(int) $report_data->total,
(int) $report_data->page_no,
(int) $report_data->pages
);
}
/**
* Prepare a report object for serialization.
*
* @param array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$response = parent::prepare_item_for_response( $report, $request );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_variations_stats', $response, $report, $request );
}
/**
* Get the Report's item properties schema.
* Will be used by `get_item_schema` as `totals` and `subtotals`.
*
* @return array
*/
protected function get_item_properties_schema() {
return array(
'items_sold' => array(
'title' => __( 'Variations Sold', 'woocommerce' ),
'description' => __( 'Number of variation items sold.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'indicator' => true,
),
'net_revenue' => array(
'description' => __( 'Net sales.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'format' => 'currency',
),
'orders_count' => array(
'description' => __( 'Number of orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['title'] = 'report_variations_stats';
$segment_label = array(
'description' => __( 'Human readable segment label, either product or variation name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
);
$schema['properties']['totals']['properties']['segments']['items']['properties']['segment_label'] = $segment_label;
$schema['properties']['intervals']['items']['properties']['subtotals']['properties']['segments']['items']['properties']['segment_label'] = $segment_label;
return $this->add_additional_fields_schema( $schema );
}
/**
* Set the default results to 0 if API returns an empty array
*
* @param Mixed $results Report data.
* @return object
*/
public function set_default_report_data( $results ) {
if ( empty( $results ) ) {
$results = new \stdClass();
$results->total = 0;
$results->totals = new \stdClass();
$results->totals->items_sold = 0;
$results->totals->net_revenue = 0;
$results->totals->orders_count = 0;
$results->intervals = array();
$results->pages = 1;
$results->page_no = 1;
}
return $results;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
'type' => 'string',
'default' => 'all',
'enum' => array(
'all',
'any',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby']['enum'] = array(
'date',
'net_revenue',
'coupons',
'refunds',
'shipping',
'taxes',
'net_revenue',
'orders_count',
'items_sold',
);
$params['category_includes'] = array(
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['category_excludes'] = array(
'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['variations'] = array(
'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'woocommerce' ),
'type' => 'string',
'enum' => array(
'product',
'category',
'variation',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['fields'] = array(
'description' => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
$params['attribute_is'] = array(
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'array',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_is_not'] = array(
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'array',
),
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Stats/DataStore.php 0000644 00000026356 15155233514 0010261 0 ustar 00 <?php
/**
* API\Reports\Products\Stats\DataStore class file.
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* API\Reports\Variations\Stats\DataStore.
*/
class DataStore extends VariationsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'items_sold' => 'intval',
'net_revenue' => 'floatval',
'orders_count' => 'intval',
'variations_count' => 'intval',
);
/**
* Cache identifier.
*
* @var string
*/
protected $cache_key = 'variations_stats';
/**
* Data store context used to pass to filters.
*
* @var string
*/
protected $context = 'variations_stats';
/**
* Assign report columns once full table name has been assigned.
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
$this->report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
);
}
/**
* Updates the database query with parameters used for Products Stats report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
*/
protected function update_sql_query_params( $query_args ) {
global $wpdb;
$products_where_clause = '';
$products_from_clause = '';
$where_subquery = array();
$order_product_lookup_table = self::get_db_table_name();
$order_item_meta_table = $wpdb->prefix . 'woocommerce_order_itemmeta';
$included_products = $this->get_included_products( $query_args );
if ( $included_products ) {
$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
}
$excluded_products = $this->get_excluded_products( $query_args );
if ( $excluded_products ) {
$products_where_clause .= "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})";
}
$included_variations = $this->get_included_variations( $query_args );
if ( $included_variations ) {
$products_where_clause .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
} elseif ( $this->should_exclude_simple_products( $query_args ) ) {
$products_where_clause .= " AND {$order_product_lookup_table}.variation_id != 0";
}
$order_status_filter = $this->get_status_subquery( $query_args );
if ( $order_status_filter ) {
$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$products_where_clause .= " AND ( {$order_status_filter} )";
}
$attribute_order_items_subquery = $this->get_order_item_by_attribute_subquery( $query_args );
if ( $attribute_order_items_subquery ) {
// JOIN on product lookup if we haven't already.
if ( ! $order_status_filter ) {
$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
}
// Add subquery for matching attributes to WHERE.
$products_where_clause .= $attribute_order_items_subquery;
}
if ( 0 < count( $where_subquery ) ) {
$operator = $this->get_match_operator( $query_args );
$products_where_clause .= 'AND (' . implode( " {$operator} ", $where_subquery ) . ')';
}
$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
$this->total_query->add_sql_clause( 'where', $products_where_clause );
$this->total_query->add_sql_clause( 'join', $products_from_clause );
$this->add_intervals_sql_params( $query_args, $order_product_lookup_table );
$this->interval_query->add_sql_clause( 'where', $products_where_clause );
$this->interval_query->add_sql_clause( 'join', $products_from_clause );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
}
/**
* Returns if simple products should be excluded from the report.
*
* @internal
*
* @param array $query_args Query parameters.
*
* @return boolean
*/
protected function should_exclude_simple_products( array $query_args ) {
return apply_filters( 'experimental_woocommerce_analytics_variations_stats_should_exclude_simple_products', true, $query_args );
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @since 3.5.0
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_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',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'interval' => 'week',
'product_includes' => array(),
'variation_includes' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$this->update_sql_query_params( $query_args );
$this->get_limit_sql_params( $query_args );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
);
/* phpcs:enable */
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$intervals = array();
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'order_by' => $this->get_sql_clause( 'order_by' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$totals = (object) $this->cast_numbers( $totals[0] );
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* 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;
}
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
}
Stats/Query.php 0000644 00000002446 15155233514 0007472 0 ustar 00 <?php
/**
* Class for parameter-based Variations Stats Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'categories' => array(15, 18),
* 'product_ids' => array(1,2,3)
* );
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Query( $args );
* $mydata = $report->get_data();
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* API\Reports\Variations\Stats\Query
*/
class Query extends ReportsQuery {
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get variations data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_variations_stats_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-variations-stats' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_variations_stats_select_query', $results, $args );
}
}
Stats/Segmenter.php 0000644 00000017667 15155233514 0010331 0 ustar 00 <?php
/**
* Class for adding segmenting support without cluttering the data stores.
*/
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
/**
* Date & time interval and numeric range handling class for Reporting API.
*/
class Segmenter extends ReportsSegmenter {
/**
* Returns column => query mapping to be used for product-related product-level segmenting query
* (e.g. products sold, revenue from product X when segmenting by category).
*
* @param string $products_table Name of SQL table containing the product-level segmenting info.
*
* @return array Column => SELECT query mapping.
*/
protected function get_segment_selections_product_level( $products_table ) {
$columns_mapping = array(
'items_sold' => "SUM($products_table.product_qty) as items_sold",
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
'variations_count' => "COUNT( DISTINCT $products_table.variation_id ) AS variations_count",
);
return $columns_mapping;
}
/**
* Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $totals_query Array of SQL clauses for totals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
global $wpdb;
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
// Product-level numbers.
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$segments_products = $wpdb->get_results(
"SELECT
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
FROM
$table_name
$segmenting_from
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_time_clause']}
{$totals_query['where_clause']}
$segmenting_where
GROUP BY
$segmenting_groupby",
ARRAY_A
);
/* phpcs:enable */
$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
return $totals_segments;
}
/**
* Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
*
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
* @param string $segmenting_from FROM part of segmenting SQL query.
* @param string $segmenting_where WHERE part of segmenting SQL query.
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
* @param string $segmenting_dimension_name Name of the segmenting dimension.
* @param string $table_name Name of SQL table which is the stats table for orders.
* @param array $intervals_query Array of SQL clauses for intervals query.
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
*
* @return array
*/
protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
global $wpdb;
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
// LIMIT offset, rowcount needs to be updated to a multiple of the number of segments.
preg_match( '/LIMIT (\d+)\s?,\s?(\d+)/', $intervals_query['limit'], $limit_parts );
$segment_count = count( $this->get_all_segments() );
$orig_offset = intval( $limit_parts[1] );
$orig_rowcount = intval( $limit_parts[2] );
$segmenting_limit = $wpdb->prepare( 'LIMIT %d, %d', $orig_offset * $segment_count, $orig_rowcount * $segment_count );
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
// Product-level numbers.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$segments_products = $wpdb->get_results(
"SELECT
{$intervals_query['select_clause']} AS time_interval,
$segmenting_groupby AS $segmenting_dimension_name
{$segmenting_selections['product_level']}
FROM
$table_name
$segmenting_from
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_time_clause']}
{$intervals_query['where_clause']}
$segmenting_where
GROUP BY
time_interval, $segmenting_groupby
$segmenting_limit",
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
ARRAY_A
);
$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
return $intervals_segments;
}
/**
* Return array of segments formatted for REST response.
*
* @param string $type Type of segments to return--'totals' or 'intervals'.
* @param array $query_params SQL query parameter array.
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
*
* @return array
* @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
*/
protected function get_segments( $type, $query_params, $table_name ) {
global $wpdb;
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
return array();
}
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
$unique_orders_table = 'uniq_orders';
$segmenting_where = '';
// Product, variation, and category are bound to product, so here product segmenting table is required,
// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
// This also means that segment selections need to be calculated differently.
if ( 'variation' === $this->query_args['segmentby'] ) {
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
$segmenting_selections = array(
'product_level' => $this->prepare_selections( $product_level_columns ),
);
$this->report_columns = $product_level_columns;
$segmenting_from = '';
$segmenting_groupby = $product_segmenting_table . '.variation_id';
$segmenting_dimension_name = 'variation_id';
// Restrict our search space for variation comparisons.
if ( isset( $this->query_args['variation_includes'] ) ) {
$variation_ids = implode( ',', $this->get_all_segments() );
$segmenting_where = " AND $product_segmenting_table.variation_id IN ( $variation_ids )";
}
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
}
return $segments;
}
}