Reduce duplicated code in Analytics classes (#49425)

- Add code docs to the Analytics classes and make get_order_statuses non-internal.
   Document analytics classes and some methods. Add simple examples of how to use it. Give a brief "why?" and "when?" answers. Add links between related classes.
- Replace `Automattic\WooCommerce\Admin\API\Reports\*\Query` classes with a single `GenericQuery` class.
- Reduce the amount of duplicated code in Analytics `DataStore`s.
- Reduce duplicated code in Analytics controllers
- Add `FilteredGetDataTrait`, `OrderAwareControllerTrait`, and `StatsDataStoreTrait` for extension developers to reuse while creating custom Analytics
- Add a `GenericQuery` to reduce duplicated code in Query classes.
   Also, to expose it to community extensions for reuse.
- Use `GenericQuery` instead of duplicated `Query` classes
- Move caching code to shared `DataStore::get_data`
- Move intervals specific `initialize_queries` to shared Trait
- Move intervals checking logic to shared `StatsDataStoreTrait`
- Reuse `GenericController::prepare_item_for_response` for `Reports\Controller` subclasses
- Reuse `GenericController::get_collection_params` for `Reports\Controller` subclasses.
- Move `get_items` code to shared `GenericStatsController`
- Move shared paginable controllers `get_items` code to the generic class
- Move fields param to `GenericStatsController`
- Separate `OrderAwareControllerTrait` from `ReportController`
   to allow specific Analytics Controllers to extend the `Generic(Stats)Controller` directly, without extending the `ReportController`, which is used to list reports.


It's meant not to change any behavior. However, due to unification, it did tweak a few things mostly from the developer perspective:
- Previously, only some controllers threw an error from `get_items` when the data was missing; some did not. Now controllers the following Controllers changed behavior to also for such an error:
	- Coupons
	- Customers
	- Downloads
	- Orders
	- Products
	- Taxes
- the error has one error code from all controllers:`woocommerce_rest_reports_invalid_response` (previously there were report-specific)
   - `woocommerce_rest_reports_categories_invalid_response` -> `woocommerce_rest_reports_invalid_response`
- In Orders Controller, the `$orders_data['order_number'], $orders_data['total_formatted']` are set in the `prepare_item_for_response` method, not in the `get_items`So `prepare_item_for_response` function is expected to be called with bare data from Query
- In Products Controller extended-info is now sanitized in the `prepare_item_for_response` method, not in the `get_items`
- ⚠️ (breaking) Previosly some Controlers' `prepare_item_for_response` function was getting and expecting first argument to be casted to `object`, while the rest of controllers used data as returned from the Store. This PR unifies this behavior  for
   -  `Coupons\Stats\Controller`
   -  `Taxes\Controller`
   -  `Taxes\Stats\Controller`
   So, if you extend those classes and override `prepare_item_for_response` in a way that expects `object`, you will get an error. (I have not found such cases in the `all-plugins` repo.)
   All `woocommerce_rest_prepare_report_*` filters remain intact and inconsistent for backward compatibility.

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Mik <mikkamp@users.noreply.github.com>
This commit is contained in:
Tomek Wytrębowicz 2024-08-21 19:23:51 +02:00 committed by GitHub
parent c738aeed17
commit 0322426dce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 2931 additions and 2823 deletions

View File

@ -22,7 +22,7 @@ The `SqlQuery` class is a SQL Query statement object. Its properties consist of
## Reports Data Stores ## Reports Data Stores
The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. The implementation data store classes use the following `SqlQuery` instances: The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends the `SqlQuery` class. There is `StatsDataStoreTrait` that adds Interval & Total Queries. The implementation data store classes use the following `SqlQuery` instances:
| Data Store | Context | Class Query | Sub Query | Interval Query | Total Query | | Data Store | Context | Class Query | Sub Query | Interval Query | Total Query |
| ---------- | ------- | ----------- | --------- | -------------- | ----------- | | ---------- | ------- | ----------- | --------- | -------------- | ----------- |
@ -40,6 +40,7 @@ The base DataStore `Automattic\WooCommerce\Admin\API\Reports\DataStore` extends
| Taxes | taxes | Yes | Yes | - | - | | Taxes | taxes | Yes | Yes | - | - |
| Tax Stats | tax_stats | Yes | - | Yes | Yes | | Tax Stats | tax_stats | Yes | - | Yes | Yes |
| Variations | variations | Yes | Yes | - | - | | Variations | variations | Yes | Yes | - | - |
| StatsDataStoreTrait | n/a | n/a | - | Yes | Yes |
Query contexts are named as follows: Query contexts are named as follows:

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Replace `Automattic\WooCommerce\Admin\API\Reports\*\Query` classes with a single `GenericQuery` class.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Reduce the amount of duplicated code in Analytics `DataStore`s.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add `FilteredGetDataTrait`, `OrderAwareControllerTrait`, and `StatsDataStoreTrait` for extension developers to reuse while creating custom Analytics

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Added code docs with examples to the Analytics classes

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Reduce duplicated code in Analytics controllers, unify their behavior and API.

View File

@ -9,16 +9,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
defined( 'ABSPATH' ) || exit; 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\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/** /**
* REST API Reports categories controller class. * REST API Reports categories controller class.
* *
* @internal * @internal
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller * @extends \Automattic\WooCommerce\Admin\API\Reports\GenericController
*/ */
class Controller extends ReportsController implements ExportableInterface { class Controller extends GenericController implements ExportableInterface {
use OrderAwareControllerTrait;
/** /**
* Route base. * Route base.
@ -27,6 +31,19 @@ class Controller extends ReportsController implements ExportableInterface {
*/ */
protected $rest_base = 'reports/categories'; protected $rest_base = 'reports/categories';
/**
* Get data from `'categories'` Query.
*
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'categories' );
return $query->get_data();
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -52,56 +69,15 @@ class Controller extends ReportsController implements ExportableInterface {
} }
/** /**
* Get all reports. * Prepare a report data item for serialization.
* *
* @param WP_REST_Request $request Request data. * @param mixed $report Report data item as returned from Data Store.
* @return array|WP_Error * @param \WP_REST_Request $request Request object.
*/ * @return \WP_REST_Response
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$categories_query = new Query( $query_args );
$report_data = $categories_query->get_data();
if ( is_wp_error( $report_data ) ) {
return $report_data;
}
if ( ! isset( $report_data->data ) || ! isset( $report_data->page_no ) || ! isset( $report_data->pages ) ) {
return new \WP_Error( 'woocommerce_rest_reports_categories_invalid_response', __( 'Invalid response from data store.', 'woocommerce' ), array( 'status' => 500 ) );
}
$out_data = array();
foreach ( $report_data->data as $datum ) {
$item = $this->prepare_item_for_response( $datum, $request );
$out_data[] = $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 stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { 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. // Wrap the data in a response object.
$response = rest_ensure_response( $data ); $response = parent::prepare_item_for_response( $report, $request );
$response->add_links( $this->prepare_links( $report ) ); $response->add_links( $this->prepare_links( $report ) );
/** /**
@ -119,7 +95,7 @@ class Controller extends ReportsController implements ExportableInterface {
/** /**
* Prepare links for the request. * Prepare links for the request.
* *
* @param \Automattic\WooCommerce\Admin\API\Reports\Query $object Object data. * @param \Automattic\WooCommerce\Admin\API\Reports\GenericQuery $object Object data.
* @return array * @return array
*/ */
protected function prepare_links( $object ) { protected function prepare_links( $object ) {
@ -193,59 +169,17 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$params = array(); $params = parent::get_collection_params();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); $params['orderby']['default'] = 'category_id';
$params['page'] = array( $params['orderby']['enum'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ), 'category_id',
'type' => 'integer', 'items_sold',
'default' => 1, 'net_revenue',
'sanitize_callback' => 'absint', 'orders_count',
'validate_callback' => 'rest_validate_request_arg', 'products_count',
'minimum' => 1, 'category',
); );
$params['per_page'] = array( $params['interval'] = 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['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' => 'category_id',
'enum' => array(
'category_id',
'items_sold',
'net_revenue',
'orders_count',
'products_count',
'category',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ), 'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string', 'type' => 'string',
'default' => 'week', 'default' => 'week',
@ -259,7 +193,7 @@ class Controller extends ReportsController implements ExportableInterface {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['status_is'] = array( $params['status_is'] = array(
'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce' ), 'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list', 'sanitize_callback' => 'wp_parse_slug_list',
@ -269,7 +203,7 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'string', 'type' => 'string',
), ),
); );
$params['status_is_not'] = array( $params['status_is_not'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce' ), 'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list', 'sanitize_callback' => 'wp_parse_slug_list',
@ -279,7 +213,7 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'string', 'type' => 'string',
), ),
); );
$params['categories'] = array( $params['categories'] = array(
'description' => __( 'Limit result set to all items that have the specified term assigned in the categories taxonomy.', 'woocommerce' ), 'description' => __( 'Limit result set to all items that have the specified term assigned in the categories taxonomy.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
@ -288,19 +222,13 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['extended_info'] = array( $params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each category to the report.', 'woocommerce' ), 'description' => __( 'Add additional piece of info about each category to the report.', 'woocommerce' ),
'type' => 'boolean', 'type' => 'boolean',
'default' => false, 'default' => false,
'sanitize_callback' => 'wc_string_to_bool', 'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$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; return $params;
} }

View File

@ -9,7 +9,6 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore; use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/** /**
@ -20,6 +19,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_product_lookup'; protected static $table_name = 'wc_order_product_lookup';
@ -27,6 +28,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'categories'; protected $cache_key = 'categories';
@ -48,6 +51,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -61,12 +66,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'categories'; protected $context = 'categories';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -145,6 +154,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* @return string * @return string
*/ */
@ -201,104 +212,99 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['category_includes'] = array();
$defaults['extended_info'] = false;
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @see get_data
* @override ReportsDataStore::get_noncached_data()
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
$this->initialize_queries();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $data = (object) array(
$defaults = array( 'data' => array(),
'per_page' => get_option( 'posts_per_page' ), 'total' => 0,
'page' => 1, 'pages' => 0,
'order' => 'DESC', 'page_no' => 0,
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'extended_info' => false,
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
* We need to get the cache key here because $included_categories = $this->get_included_categories_array( $query_args );
* parent::update_intervals_sql_params() modifies $query_args. $this->add_sql_query_params( $query_args );
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) { if ( count( $included_categories ) > 0 ) {
$this->initialize_queries(); $fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_categories, 'category_id' );
$data = (object) array( $this->add_sql_clause( 'select', $this->format_join_selections( array_merge( array( 'category_id' ), $fields ), array( 'category_id' ) ) );
'data' => array(), $this->add_sql_clause( 'from', '(' );
'total' => 0, $this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
'pages' => 0, $this->add_sql_clause( 'from', ") AS {$table_name}" );
'page_no' => 0, $this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.category_id = {$table_name}.category_id"
); );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) ); $categories_query = $this->get_query_statement();
$included_categories = $this->get_included_categories_array( $query_args ); } else {
$this->add_sql_query_params( $query_args ); $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$categories_query = $this->subquery->get_query_statement();
if ( count( $included_categories ) > 0 ) {
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_categories, 'category_id' );
$this->add_sql_clause( 'select', $this->format_join_selections( array_merge( array( 'category_id' ), $fields ), array( 'category_id' ) ) );
$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.category_id = {$table_name}.category_id"
);
$categories_query = $this->get_query_statement();
} else {
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$categories_query = $this->subquery->get_query_statement();
}
$categories_data = $wpdb->get_results(
$categories_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $categories_data ) {
return new \WP_Error( 'woocommerce_analytics_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
$record_count = count( $categories_data );
$total_pages = (int) ceil( $record_count / $query_args['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$categories_data = $this->page_records( $categories_data, $query_args['page'], $query_args['per_page'] );
$this->include_extended_info( $categories_data, $query_args );
$categories_data = array_map( array( $this, 'cast_numbers' ), $categories_data );
$data = (object) array(
'data' => $categories_data,
'total' => $record_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
} }
$categories_data = $wpdb->get_results(
$categories_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $categories_data ) {
return new \WP_Error( 'woocommerce_analytics_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
$record_count = count( $categories_data );
$total_pages = (int) ceil( $record_count / $query_args['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$categories_data = $this->page_records( $categories_data, $query_args['page'], $query_args['per_page'] );
$this->include_extended_info( $categories_data, $query_args );
$categories_data = array_map( array( $this, 'cast_numbers' ), $categories_data );
$data = (object) array(
'data' => $categories_data,
'total' => $record_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
return $data; return $data;
} }
/** /**
* Initialize query objects. * Initialize query objects.
*
* @override ReportsDataStore::initialize_queries()
*/ */
protected function initialize_queries() { protected function initialize_queries() {
global $wpdb; global $wpdb;

View File

@ -21,7 +21,9 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery; use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Query * API\Reports\Categories\Query
*
* @deprecated 9.3.0 Categories\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
@ -30,6 +32,8 @@ class Query extends ReportsQuery {
/** /**
* Valid fields for Categories report. * Valid fields for Categories report.
* *
* @deprecated 9.3.0 Categories\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -39,6 +43,8 @@ class Query extends ReportsQuery {
/** /**
* Get categories data based on the current query vars. * Get categories data based on the current query vars.
* *
* @deprecated 9.3.0 Categories\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -1,8 +1,6 @@
<?php <?php
/** /**
* REST API Reports controller extended by WC Admin plugin. * REST API Reports controller extended to handle requests to the reports endpoint.
*
* Handles requests to the reports endpoint.
*/ */
namespace Automattic\WooCommerce\Admin\API\Reports; namespace Automattic\WooCommerce\Admin\API\Reports;
@ -10,15 +8,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController; use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/** /**
* REST API Reports controller class. * Reports controller class.
*
* Controller that handles the endpoint that returns all available analytics endpoints.
* *
* @internal * @internal
* @extends GenericController * @extends GenericController
*/ */
class Controller extends GenericController { class Controller extends GenericController {
use OrderAwareControllerTrait;
/** /**
* Get all reports. * Get all reports.
* *
@ -135,71 +138,6 @@ class Controller extends GenericController {
return rest_ensure_response( $data ); return rest_ensure_response( $data );
} }
/**
* Get the order number for an order. If no filter is present for `woocommerce_order_number`, we can just return the ID.
* Returns the parent order number if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string|null The Order Number or null if the order doesn't exist.
*/
protected function get_order_number( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
// If the parent order doesn't exist, return null.
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
}
if ( ! has_filter( 'woocommerce_order_number' ) ) {
return $order->get_id();
}
return $order->get_order_number();
}
/**
* Whether the order is valid.
*
* @param bool|WC_Order|WC_Order_Refund $order Order object.
* @return bool True if the order is valid, false otherwise.
*/
protected function is_valid_order( $order ) {
return $order instanceof \WC_Order || $order instanceof \WC_Order_Refund;
}
/**
* Get the order total with the related currency formatting.
* Returns the parent order total if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string|null The Order Number or null if the order doesn't exist.
*/
protected function get_total_formatted( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
}
return wp_strip_all_tags( html_entity_decode( $order->get_formatted_order_total() ), true );
}
/** /**
* Prepare a report object for serialization. * Prepare a report object for serialization.
* *
@ -214,12 +152,8 @@ class Controller extends GenericController {
'path' => $report->path, 'path' => $report->path,
); );
$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. // Wrap the data in a response object.
$response = rest_ensure_response( $data ); $response = parent::prepare_item_for_response( $data, $request );
$response->add_links( $response->add_links(
array( array(
'self' => array( 'self' => array(
@ -249,6 +183,8 @@ class Controller extends GenericController {
/** /**
* Get the Report's schema, conforming to JSON Schema. * Get the Report's schema, conforming to JSON Schema.
* *
* @override WP_REST_Controller::get_item_schema()
*
* @return array * @return array
*/ */
public function get_item_schema() { public function get_item_schema() {
@ -291,42 +227,4 @@ class Controller extends GenericController {
'context' => $this->get_context_param( array( 'default' => 'view' ) ), 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
); );
} }
/**
* Get order statuses without prefixes.
* Includes unregistered statuses that have been marked "actionable".
*
* @internal
* @return array
*/
public static function get_order_statuses() {
// Allow all statuses selected as "actionable" - this may include unregistered statuses.
// See: https://github.com/woocommerce/woocommerce-admin/issues/5592.
$actionable_statuses = get_option( 'woocommerce_actionable_order_statuses', array() );
// See WC_REST_Orders_V2_Controller::get_collection_params() re: any/trash statuses.
$registered_statuses = array_merge( array( 'any', 'trash' ), array_keys( self::get_order_status_labels() ) );
// Merge the status arrays (using flip to avoid array_unique()).
$allowed_statuses = array_keys( array_merge( array_flip( $registered_statuses ), array_flip( $actionable_statuses ) ) );
return $allowed_statuses;
}
/**
* Get order statuses (and labels) without prefixes.
*
* @internal
* @return array
*/
public static function get_order_status_labels() {
$order_statuses = array();
foreach ( wc_get_order_statuses() as $key => $label ) {
$new_key = str_replace( 'wc-', '', $key );
$order_statuses[ $new_key ] = $label;
}
return $order_statuses;
}
} }

View File

@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController; use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface; use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -29,6 +30,19 @@ class Controller extends GenericController implements ExportableInterface {
*/ */
protected $rest_base = 'reports/coupons'; protected $rest_base = 'reports/coupons';
/**
* Get data from `'coupons'` Query.
*
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'coupons' );
return $query->get_data();
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -50,38 +64,11 @@ class Controller extends GenericController implements ExportableInterface {
} }
/** /**
* Get all reports. * Prepare a report data item for serialization.
* *
* @param WP_REST_Request $request Request data. * @param array $report Report data item as returned from Data Store.
* @return array|WP_Error * @param \WP_REST_Request $request Request object.
*/ * @return \WP_REST_Response
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$coupons_query = new Query( $query_args );
$report_data = $coupons_query->get_data();
$data = array();
foreach ( $report_data->data as $coupons_data ) {
$item = $this->prepare_item_for_response( $coupons_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$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 ) { public function prepare_item_for_response( $report, $request ) {
$response = parent::prepare_item_for_response( $report, $request ); $response = parent::prepare_item_for_response( $report, $request );

View File

@ -21,6 +21,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_coupon_lookup'; protected static $table_name = 'wc_order_coupon_lookup';
@ -28,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'coupons'; protected $cache_key = 'coupons';
@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -46,12 +52,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'coupons'; protected $context = 'coupons';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -148,6 +158,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* @return string * @return string
*/ */
@ -223,119 +235,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
} }
/**
* 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' => 'coupon_id',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'coupons' => 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_coupons = $this->get_included_coupons_array( $query_args );
$limit_params = $this->get_limit_params( $query_args );
$this->subquery->add_sql_clause( 'select', $selections );
$this->add_sql_query_params( $query_args );
if ( count( $included_coupons ) > 0 ) {
$total_results = count( $included_coupons );
$total_pages = (int) ceil( $total_results / $limit_params['per_page'] );
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_coupons, 'coupon_id' );
$this->add_sql_clause( 'select', $this->format_join_selections( $fields, array( 'coupon_id' ) ) );
$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.coupon_id = {$table_name}.coupon_id"
);
$coupons_query = $this->get_query_statement();
} else {
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$coupons_query = $this->subquery->get_query_statement();
$this->subquery->clear_sql_clause( array( 'select', 'order_by', 'limit' ) );
$this->subquery->add_sql_clause( 'select', 'coupon_id' );
$coupon_subquery = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt";
$db_records_count = (int) $wpdb->get_var(
$coupon_subquery // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
}
$coupon_data = $wpdb->get_results(
$coupons_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $coupon_data ) {
return $data;
}
$this->include_extended_info( $coupon_data, $query_args );
$coupon_data = array_map( array( $this, 'cast_numbers' ), $coupon_data );
$data = (object) array(
'data' => $coupon_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/** /**
* Get coupon ID for an order. * Get coupon ID for an order.
* *
@ -363,6 +262,115 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
return wc_get_coupon_id_by_code( $coupon_item->get_code() ); return wc_get_coupon_id_by_code( $coupon_item->get_code() );
} }
/**
* Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
*
* @override ReportsDataStore::get_default_query_vars()
*
* @return array Query parameters.
*/
public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['orderby'] = 'coupon_id';
$defaults['coupons'] = array();
$defaults['extended_info'] = false;
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$included_coupons = $this->get_included_coupons_array( $query_args );
$limit_params = $this->get_limit_params( $query_args );
$this->subquery->add_sql_clause( 'select', $selections );
$this->add_sql_query_params( $query_args );
if ( count( $included_coupons ) > 0 ) {
$total_results = count( $included_coupons );
$total_pages = (int) ceil( $total_results / $limit_params['per_page'] );
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_coupons, 'coupon_id' );
$this->add_sql_clause( 'select', $this->format_join_selections( $fields, array( 'coupon_id' ) ) );
$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.coupon_id = {$table_name}.coupon_id"
);
$coupons_query = $this->get_query_statement();
} else {
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$coupons_query = $this->subquery->get_query_statement();
$this->subquery->clear_sql_clause( array( 'select', 'order_by', 'limit' ) );
$this->subquery->add_sql_clause( 'select', 'coupon_id' );
$coupon_subquery = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt";
$db_records_count = (int) $wpdb->get_var(
$coupon_subquery // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
}
$coupon_data = $wpdb->get_results(
$coupons_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $coupon_data ) {
return $data;
}
$this->include_extended_info( $coupon_data, $query_args );
$coupon_data = array_map( array( $this, 'cast_numbers' ), $coupon_data );
$data = (object) array(
'data' => $coupon_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
return $data;
}
/** /**
* Create or update an an entry in the wc_order_coupon_lookup table for an order. * Create or update an an entry in the wc_order_coupon_lookup table for an order.
* *

View File

@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Coupons\Query * API\Reports\Coupons\Query
*
* @deprecated 9.3.0 Coupons\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Products report. * Valid fields for Products report.
* *
* @deprecated 9.3.0 Coupons\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Coupons\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -10,7 +10,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController; use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException; use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -54,51 +54,30 @@ class Controller extends GenericStatsController {
} }
/** /**
* Get all reports. * Get data from `'coupons-stats'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return array|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query_args = $this->prepare_reports_query( $request ); $query = new GenericQuery( $query_args, 'coupons-stats' );
$coupons_query = new Query( $query_args ); return $query->get_data();
try {
$report_data = $coupons_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( (object) $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. * Prepare a report data item for serialization.
* *
* @param stdClass $report Report data. * @param mixed $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report ); $response = parent::prepare_item_for_response( $report, $request );
$response = parent::prepare_item_for_response( $data, $request );
// Map to `object` for backwards compatibility.
$report = (object) $report;
/** /**
* Filter a report returned from the API. * Filter a report returned from the API.
* *
@ -189,15 +168,6 @@ class Controller extends GenericStatsController {
), ),
'validate_callback' => 'rest_validate_request_arg', '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',
),
);
return $params; return $params;
} }

View File

@ -9,15 +9,19 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/** /**
* API\Reports\Coupons\Stats\DataStore. * API\Reports\Coupons\Stats\DataStore.
*/ */
class DataStore extends CouponsDataStore implements DataStoreInterface { class DataStore extends CouponsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override CouponsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -33,6 +37,8 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
/** /**
* SQL columns to select in the db query. * SQL columns to select in the db query.
* *
* @override CouponsDataStore::$report_columns
*
* @var array * @var array
*/ */
protected $report_columns; protected $report_columns;
@ -40,6 +46,8 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override CouponsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'coupons_stats'; protected $context = 'coupons_stats';
@ -47,12 +55,16 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override CouponsDataStore::get_default_query_vars()
*
* @var string * @var string
*/ */
protected $cache_key = 'coupons_stats'; protected $cache_key = 'coupons_stats';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override CouponsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -105,145 +117,114 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @since 3.5.0 * @override CouponsDataStore::get_default_query_vars()
* @param array $query_args Query parameters. *
* @return stdClass|WP_Error Data. * @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['coupons'] = array();
$defaults['interval'] = 'week';
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override CouponsDataStore::get_noncached_stats_data()
*
* @see get_data
* @see get_noncached_stats_data
* @param array $query_args Query parameters.
* @param array $params Query limit parameters.
* @param stdClass $data Reference to the data object to fill.
* @param int $expected_interval_count Number of expected intervals.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $selections = $this->selected_columns( $query_args );
'page' => 1, $totals_query = array();
'order' => 'DESC', $intervals_query = array();
'orderby' => 'date', $limit_params = $this->get_limit_sql_params( $query_args );
'before' => TimeInterval::default_before(), $this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
'after' => TimeInterval::default_after(),
'fields' => '*', $db_intervals = $wpdb->get_col(
'interval' => 'week', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
'coupons' => array(), $this->interval_query->get_query_statement()
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $db_interval_count = count( $db_intervals );
* 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->total_query->add_sql_clause( 'select', $selections );
$this->initialize_queries(); $totals = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->total_query->get_query_statement(),
ARRAY_A
);
$data = (object) array( if ( null === $totals ) {
'data' => array(), return $data;
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$totals_query = array();
$intervals_query = array();
$limit_params = $this->get_limit_sql_params( $query_args );
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$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 / $limit_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->total_query->add_sql_clause( 'select', $selections );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return $data;
}
// @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' ),
'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 );
$totals = (object) $this->cast_numbers( $totals[0] );
// Intervals.
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$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 );
}
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return $data;
}
$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, $limit_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'], $limit_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 );
} }
// phpcs:ignore Generic.Commenting.Todo.TaskFound
// @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' ),
'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 );
$totals = (object) $this->cast_numbers( $totals[0] );
// Intervals.
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$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 );
}
$intervals = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->interval_query->get_query_statement(),
ARRAY_A
);
if ( null === $intervals ) {
return $data;
}
$data->totals = $totals;
$data->intervals = $intervals;
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $limit_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'], $limit_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 );
return $data; return $data;
} }
/**
* 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' );
}
} }

View File

@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Coupons\Stats\Query * API\Reports\Coupons\Stats\Query
*
* @deprecated 9.3.0 Coupons\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Products report. * Valid fields for Products report.
* *
* @deprecated 9.3.0 Coupons\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Coupons\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -33,6 +33,19 @@ class Controller extends GenericController implements ExportableInterface {
*/ */
protected $rest_base = 'reports/customers'; protected $rest_base = 'reports/customers';
/**
* Get data from Query.
*
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$query = new Query( $query_args );
return $query->get_data();
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -84,34 +97,6 @@ class Controller extends GenericController implements ExportableInterface {
return $args; return $args;
} }
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$customers_query = new Query( $query_args );
$report_data = $customers_query->get_data();
$data = array();
foreach ( $report_data->data as $customer_data ) {
$item = $this->prepare_item_for_response( $customer_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$data,
(int) $report_data->total,
(int) $report_data->page_no,
(int) $report_data->pages
);
}
/** /**
* Get one report. * Get one report.
* *
@ -139,11 +124,11 @@ class Controller extends GenericController implements ExportableInterface {
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param array $report Report data. * @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param \WP_REST_Request $request Request object.
* @return WP_REST_Response * @return \WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $context = ! empty( $request['context'] ) ? $request['context'] : 'view';

View File

@ -22,6 +22,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_customer_lookup'; protected static $table_name = 'wc_customer_lookup';
@ -29,6 +31,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'customers'; protected $cache_key = 'customers';
@ -36,6 +40,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -49,12 +55,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'customers'; protected $context = 'customers';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
global $wpdb; global $wpdb;
@ -168,6 +178,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* @return string * @return string
*/ */
@ -182,6 +194,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Fills WHERE clause of SQL request with date-related constraints. * Fills WHERE clause of SQL request with date-related constraints.
* *
* @override ReportsDataStore::add_time_period_sql_params()
*
* @param array $query_args Parameters supplied by the user. * @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint. * @param string $table_name Name of the db table relevant for the date constraint.
*/ */
@ -409,89 +423,20 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
global $wpdb; $defaults = parent::get_default_query_vars();
$defaults['orderby'] = 'date_registered';
$defaults['order_before'] = TimeInterval::default_before();
$defaults['order_after'] = TimeInterval::default_after();
$customers_table_name = self::get_db_table_name(); return $defaults;
$order_stats_table_name = $wpdb->prefix . 'wc_order_stats';
// 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',
'order_before' => TimeInterval::default_before(),
'order_after' => TimeInterval::default_after(),
'fields' => '*',
);
$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 );
$sql_query_params = $this->add_sql_query_params( $query_args );
$count_query = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) as tt
";
$db_records_count = (int) $wpdb->get_var(
$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$customer_data = $wpdb->get_results(
$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
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'],
);
$this->set_cached_data( $cache_key, $data );
}
return $data;
} }
/** /**
@ -533,6 +478,69 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
} }
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb;
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->add_sql_query_params( $query_args );
$count_query = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) as tt
";
$db_records_count = (int) $wpdb->get_var(
$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$customer_data = $wpdb->get_results(
$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
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'],
);
return $data;
}
/** /**
* Get or create a customer from a given order. * Get or create a customer from a given order.
* *

View File

@ -16,14 +16,23 @@
namespace Automattic\WooCommerce\Admin\API\Reports\Customers; namespace Automattic\WooCommerce\Admin\API\Reports\Customers;
defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery; defined( 'ABSPATH' ) || exit;
/** /**
* API\Reports\Customers\Query * API\Reports\Customers\Query
*/ */
class Query extends ReportsQuery { class Query extends GenericQuery {
/**
* Specific query name.
* Will be used to load the `report-{name}` data store,
* and to call `woocommerce_analytics_{snake_case(name)}_*` filters.
*
* @var string
*/
protected $name = 'customers';
/** /**
* Valid fields for Customers report. * Valid fields for Customers report.
@ -39,17 +48,4 @@ class Query extends ReportsQuery {
'fields' => '*', 'fields' => '*',
); );
} }
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_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_analytics_customers_select_query', $results, $args );
}
} }

View File

@ -7,6 +7,8 @@
namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats; namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;
use Automattic\WooCommerce\Admin\API\Reports\Customers\Query;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
@ -83,7 +85,7 @@ class Controller extends \WC_REST_Reports_Controller {
*/ */
public function get_items( $request ) { public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request ); $query_args = $this->prepare_reports_query( $request );
$customers_query = new Query( $query_args ); $customers_query = new Query( $query_args, 'customers-stats' );
$report_data = $customers_query->get_data(); $report_data = $customers_query->get_data();
$out_data = array( $out_data = array(
'totals' => $report_data, 'totals' => $report_data,
@ -93,11 +95,11 @@ class Controller extends \WC_REST_Reports_Controller {
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param Array $report Report data. * @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param \WP_REST_Request $request Request object.
* @return WP_REST_Response * @return \WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report, $request ) {
$data = $report; $data = $report;

View File

@ -8,6 +8,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
/** /**
@ -17,6 +18,8 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override CustomersDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -29,6 +32,8 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override CustomersDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'customers_stats'; protected $cache_key = 'customers_stats';
@ -36,12 +41,16 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override CustomersDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'customers_stats'; protected $context = 'customers_stats';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override CustomersDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$this->report_columns = array( $this->report_columns = array(
@ -53,76 +62,70 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override CustomersDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = ReportsDataStore::get_default_query_vars();
$defaults['orderby'] = 'date_registered';
// Do not set `order_before` and `order_after` here, like in the parent class.
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override CustomersDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
$this->initialize_queries();
$customers_table_name = self::get_db_table_name(); $data = (object) array(
'customers_count' => 0,
// These defaults are only partially applied when used via REST API, as that has its own defaults. 'avg_orders_count' => 0,
$defaults = array( 'avg_total_spend' => 0.0,
'per_page' => get_option( 'posts_per_page' ), 'avg_avg_order_value' => 0.0,
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_registered',
'fields' => '*',
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $selections = $this->selected_columns( $query_args );
* We need to get the cache key here because $this->add_sql_query_params( $query_args );
* parent::update_intervals_sql_params() modifies $query_args. // Clear SQL clauses set for parent class queries that are different here.
*/ $this->subquery->clear_sql_clause( 'select' );
$cache_key = $this->get_cache_key( $query_args ); $this->subquery->add_sql_clause( 'select', 'SUM( total_sales ) AS total_spend,' );
$data = $this->get_cached_data( $cache_key ); $this->subquery->add_sql_clause(
'select',
'SUM( CASE WHEN parent_id = 0 THEN 1 END ) as orders_count,'
);
$this->subquery->add_sql_clause(
'select',
'CASE WHEN SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) END AS avg_order_value'
);
if ( false === $data ) { $this->clear_sql_clause( array( 'order_by', 'limit' ) );
$this->initialize_queries(); $this->add_sql_clause( 'select', $selections );
$this->add_sql_clause( 'from', "({$this->subquery->get_query_statement()}) AS tt" );
$data = (object) array( $report_data = $wpdb->get_results(
'customers_count' => 0, $this->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
'avg_orders_count' => 0, ARRAY_A
'avg_total_spend' => 0.0, );
'avg_avg_order_value' => 0.0,
);
$selections = $this->selected_columns( $query_args ); if ( null === $report_data ) {
$this->add_sql_query_params( $query_args ); return $data;
// Clear SQL clauses set for parent class queries that are different here.
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', 'SUM( total_sales ) AS total_spend,' );
$this->subquery->add_sql_clause(
'select',
'SUM( CASE WHEN parent_id = 0 THEN 1 END ) as orders_count,'
);
$this->subquery->add_sql_clause(
'select',
'CASE WHEN SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) = 0 THEN NULL ELSE SUM( total_sales ) / SUM( CASE WHEN parent_id = 0 THEN 1 ELSE 0 END ) END AS avg_order_value'
);
$this->clear_sql_clause( array( 'order_by', 'limit' ) );
$this->add_sql_clause( 'select', $selections );
$this->add_sql_clause( 'from', "({$this->subquery->get_query_statement()}) AS tt" );
$report_data = $wpdb->get_results(
$this->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $report_data ) {
return $data;
}
$data = (object) $this->cast_numbers( $report_data[0] );
$this->set_cached_data( $cache_key, $data );
} }
$data = (object) $this->cast_numbers( $report_data[0] );
return $data; return $data;
} }
} }

View File

@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Customers\Stats\Query * API\Reports\Customers\Stats\Query
*
* @deprecated 9.3.0 Customers\Stats\Query class is deprecated, please use Reports\Customers\Query with a custom name, GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Customers report. * Valid fields for Customers report.
* *
* @deprecated 9.3.0 Customers\Stats\Query class is deprecated, please use Reports\Customers\Query with a custom name, GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -43,6 +47,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Customers\Stats\Query class is deprecated, please use Reports\Customers\Query with a custom name, GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,12 +9,59 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/** /**
* Admin\API\Reports\DataStore: Common parent for custom report data stores. * Common parent for custom report data stores.
*
* We use Report DataStores to separate DB data retrieval logic from the REST API controllers.
*
* Handles caching, data normalization, intervals-related methods, and other common functionality.
* So, in your custom report DataStore class that extends this class
* you can focus on specifics by overriding the `get_noncached_data` method.
*
* Minimalistic example:
* <pre><code class="language-php">class MyDataStore extends DataStore implements DataStoreInterface {
* /** Cache identifier, used by the `DataStore` class to handle caching for you. &ast;/
* protected $cache_key = 'my_thing';
* /** Data store context used to pass to filters. &ast;/
* protected $context = 'my_thing';
* /** Table used to get the data. &ast;/
* protected static $table_name = 'my_table';
* /**
* * Method that overrides the `DataStore::get_noncached_data()` to return the report data.
* * Will be called by `get_data` if there is no data in cache.
* &ast;/
* public function get_noncached_data( $query ) {
* // Do your magic.
*
* // Then return your data in conforming object structure.
* return (object) array(
* 'data' => $product_data,
* 'total' => 1,
* 'page_no' => 1,
* 'pages' => 1,
* );
* }
* }
* </code></pre>
*
* Please use the `woocommerce_data_stores` filter to add your custom data store to the list of available ones.
* Then, your store could be accessed by Controller classes ({@see GenericController::get_datastore_data() GenericController::get_datastore_data()})
* or using {@link \WC_Data_Store::load() \WC_Data_Store::load()}.
*
* We recommend registering using the REST base name of your Controller as the key, e.g.:
* <pre><code class="language-php">add_filter( 'woocommerce_data_stores', function( $stores ) {
* $stores['reports/my-thing'] = 'MyExtension\Admin\Analytics\Rest_API\MyDataStore';
* } );
* </code></pre>
* This way, `GenericController` will pick it up automatically.
*
* Note that this class is NOT {@link https://developer.woocommerce.com/docs/how-to-manage-woocommerce-data-stores/ a CRUD data store}.
* It does not implement the {@see WC_Object_Data_Store_Interface WC_Object_Data_Store_Interface} nor extend WC_Data & WC_Data_Store_WP classes.
*/ */
class DataStore extends SqlQuery { class DataStore extends SqlQuery implements DataStoreInterface {
/** /**
* Cache group for the reports. * Cache group for the reports.
@ -90,6 +137,8 @@ class DataStore extends SqlQuery {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override SqlQuery
*
* @var string * @var string
*/ */
protected $context = 'reports'; protected $context = 'reports';
@ -138,6 +187,8 @@ class DataStore extends SqlQuery {
/** /**
* Class constructor. * Class constructor.
*
* @override SqlQuery::__construct()
*/ */
public function __construct() { public function __construct() {
self::set_db_table_name(); self::set_db_table_name();
@ -160,6 +211,54 @@ class DataStore extends SqlQuery {
} }
} }
/**
* Get the data based on args.
*
* Returns the report data based on parameters supplied by the user.
* Fetches it from cache or returns `get_noncached_data` result.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error
*/
public function get_data( $query_args ) {
$defaults = $this->get_default_query_vars();
$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 ) {
$data = $this->get_noncached_data( $query_args );
$this->set_cached_data( $cache_key, $data );
}
return $data;
}
/**
* Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
*
* @return array Query parameters.
*/
public function get_default_query_vars() {
return array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
);
}
/** /**
* Get table name from database class. * Get table name from database class.
*/ */
@ -168,6 +267,19 @@ class DataStore extends SqlQuery {
return isset( $wpdb->{static::$table_name} ) ? $wpdb->{static::$table_name} : $wpdb->prefix . static::$table_name; return isset( $wpdb->{static::$table_name} ) ? $wpdb->{static::$table_name} : $wpdb->prefix . static::$table_name;
} }
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
/* translators: %s: Method name */
return new \WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
}
/** /**
* Set table name from database class. * Set table name from database class.
*/ */

View File

@ -9,16 +9,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
defined( 'ABSPATH' ) || exit; 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\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/** /**
* REST API Reports downloads controller class. * REST API Reports downloads controller class.
* *
* @internal * @internal
* @extends Automattic\WooCommerce\Admin\API\Reports\Controller * @extends Automattic\WooCommerce\Admin\API\Reports\GenericController
*/ */
class Controller extends ReportsController implements ExportableInterface { class Controller extends GenericController implements ExportableInterface {
use OrderAwareControllerTrait;
/** /**
* Route base. * Route base.
@ -28,67 +32,40 @@ class Controller extends ReportsController implements ExportableInterface {
protected $rest_base = 'reports/downloads'; protected $rest_base = 'reports/downloads';
/** /**
* Get items. * Get data from `'downloads'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return array|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$args = array(); $query = new GenericQuery( $query_args, 'downloads' );
$registered = array_keys( $this->get_collection_params() ); return $query->get_data();
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
$args[ $param_name ] = $request[ $param_name ];
}
}
$reports = new Query( $args );
$downloads_data = $reports->get_data();
$data = array();
foreach ( $downloads_data->data as $download_data ) {
$item = $this->prepare_item_for_response( $download_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$data,
(int) $downloads_data->total,
(int) $downloads_data->page_no,
(int) $downloads_data->pages
);
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param Array $report Report data. * @param Array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { 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. // Wrap the data in a response object.
$response = rest_ensure_response( $data ); $response = parent::prepare_item_for_response( $report, $request );
$response->add_links( $this->prepare_links( $report ) ); $response->add_links( $this->prepare_links( $report ) );
$response->data['date'] = get_date_from_gmt( $data['date_gmt'], 'Y-m-d H:i:s' ); $response->data['date'] = get_date_from_gmt( $report['date_gmt'], 'Y-m-d H:i:s' );
// Figure out file name. // Figure out file name.
// Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197. // Matches https://github.com/woocommerce/woocommerce/blob/4be0018c092e617c5d2b8c46b800eb71ece9ddef/includes/class-wc-download-handler.php#L197.
$product_id = intval( $data['product_id'] ); $product_id = intval( $report['product_id'] );
$_product = wc_get_product( $product_id ); $_product = wc_get_product( $product_id );
// Make sure the product hasn't been deleted. // Make sure the product hasn't been deleted.
if ( $_product ) { if ( $_product ) {
$file_path = $_product->get_file_download_path( $data['download_id'] ); $file_path = $_product->get_file_download_path( $report['download_id'] );
$filename = basename( $file_path ); $filename = basename( $file_path );
$response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); $response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
$response->data['file_path'] = $file_path; $response->data['file_path'] = $file_path;
@ -97,9 +74,9 @@ class Controller extends ReportsController implements ExportableInterface {
$response->data['file_path'] = ''; $response->data['file_path'] = '';
} }
$customer = new \WC_Customer( $data['user_id'] ); $customer = new \WC_Customer( $report['user_id'] );
$response->data['username'] = $customer->get_username(); $response->data['username'] = $customer->get_username();
$response->data['order_number'] = $this->get_order_number( $data['order_id'] ); $response->data['order_number'] = $this->get_order_number( $report['order_id'] );
/** /**
* Filter a report returned from the API. * Filter a report returned from the API.
@ -130,6 +107,22 @@ class Controller extends ReportsController implements ExportableInterface {
return $links; return $links;
} }
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$registered = array_keys( $this->get_collection_params() );
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
$args[ $param_name ] = $request[ $param_name ];
}
}
return $args;
}
/** /**
* Get the Report's schema, conforming to JSON Schema. * Get the Report's schema, conforming to JSON Schema.
* *
@ -225,53 +218,10 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$params = array(); $params = parent::get_collection_params();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); $params['orderby']['enum'] = array(
$params['page'] = array( 'date',
'description' => __( 'Current page of the collection.', 'woocommerce' ), 'product',
'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['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',
'product',
),
'validate_callback' => 'rest_validate_request_arg',
); );
$params['match'] = array( $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: products, orders, username, ip_address.', 'woocommerce' ), '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: products, orders, username, ip_address.', 'woocommerce' ),
@ -355,12 +305,6 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'string', 'type' => 'string',
), ),
); );
$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; return $params;
} }

View File

@ -20,6 +20,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_download_log'; protected static $table_name = 'wc_download_log';
@ -27,6 +29,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'downloads'; protected $cache_key = 'downloads';
@ -34,6 +38,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -51,12 +57,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'downloads'; protected $context = 'downloads';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$this->report_columns = array( $this->report_columns = array(
@ -252,6 +262,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Gets WHERE time clause of SQL request with date-related constraints. * Gets WHERE time clause of SQL request with date-related constraints.
* *
* @override ReportsDataStore::add_time_period_sql_params()
*
* @param array $query_args Parameters supplied by the user. * @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint. * @param string $table_name Name of the db table relevant for the date constraint.
* @return string * @return string
@ -294,94 +306,89 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['orderby'] = 'timestamp';
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $this->initialize_queries();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $data = (object) array(
$defaults = array( 'data' => array(),
'per_page' => get_option( 'posts_per_page' ), 'total' => 0,
'page' => 1, 'pages' => 0,
'order' => 'DESC', 'page_no' => 0,
'orderby' => 'timestamp',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $selections = $this->selected_columns( $query_args );
* We need to get the cache key here because $this->add_sql_query_params( $query_args );
* 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 ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->initialize_queries(); $db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$data = (object) array( $params = $this->get_limit_params( $query_args );
'data' => array(), $total_pages = (int) ceil( $db_records_count / $params['per_page'] );
'total' => 0, if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
'pages' => 0, return $data;
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
// 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 WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$params = $this->get_limit_params( $query_args );
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$download_data = $wpdb->get_results(
$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $download_data ) {
return $data;
}
$download_data = array_map( array( $this, 'cast_numbers' ), $download_data );
$data = (object) array(
'data' => $download_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
} }
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$download_data = $wpdb->get_results(
$this->subquery->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $download_data ) {
return $data;
}
$download_data = array_map( array( $this, 'cast_numbers' ), $download_data );
$data = (object) array(
'data' => $download_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
return $data; return $data;
} }
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* @return string * @return string
*/ */

View File

@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Downloads\Query * API\Reports\Downloads\Query
*
* @deprecated 9.3.0 Downloads\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for downloads report. * Valid fields for downloads report.
* *
* @deprecated 9.3.0 Downloads\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/** /**
* Get downloads data based on the current query vars. * Get downloads data based on the current query vars.
* *
* @deprecated 9.3.0 Downloads\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController; use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -59,39 +60,22 @@ class Controller extends GenericStatsController {
} }
/** /**
* Get all reports. * Get data from `'downloads-stats'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return array|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query_args = $this->prepare_reports_query( $request ); $query = new GenericQuery( $query_args, 'downloads-stats' );
$downloads_query = new Query( $query_args ); return $query->get_data();
$report_data = $downloads_query->get_data();
$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. * Prepare a report data item for serialization.
* *
* @param array $report Report data. * @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
@ -110,7 +94,6 @@ class Controller extends GenericStatsController {
return apply_filters( 'woocommerce_rest_prepare_report_downloads_stats', $response, $report, $request ); return apply_filters( 'woocommerce_rest_prepare_report_downloads_stats', $response, $report, $request );
} }
/** /**
* Get the Report's item properties schema. * Get the Report's item properties schema.
* Will be used by `get_item_schema` as `totals` and `subtotals`. * Will be used by `get_item_schema` as `totals` and `subtotals`.
@ -129,6 +112,7 @@ class Controller extends GenericStatsController {
), ),
); );
} }
/** /**
* Get the Report's schema, conforming to JSON Schema. * Get the Report's schema, conforming to JSON Schema.
* It does not have the segments as in GenericStatsController. * It does not have the segments as in GenericStatsController.
@ -298,15 +282,6 @@ class Controller extends GenericStatsController {
'type' => 'string', 'type' => 'string',
), ),
); );
$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',
),
);
return $params; return $params;
} }

View File

@ -10,16 +10,19 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore as DownloadsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore as DownloadsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/** /**
* API\Reports\Downloads\Stats\DataStore. * API\Reports\Downloads\Stats\DataStore.
*/ */
class DataStore extends DownloadsDataStore implements DataStoreInterface { class DataStore extends DownloadsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override DownloadsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -29,6 +32,8 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override DownloadsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'downloads_stats'; protected $cache_key = 'downloads_stats';
@ -36,12 +41,16 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override DownloadsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'downloads_stats'; protected $context = 'downloads_stats';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override DownloadsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$this->report_columns = array( $this->report_columns = array(
@ -50,111 +59,100 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override DownloadsDataStore::default_query_args()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['interval'] = 'week';
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override DownloadsDataStore::get_noncached_data()
*
* @see get_data
* @see get_noncached_stats_data
* @param array $query_args Query parameters.
* @param array $params Query limit parameters.
* @param stdClass $data Reference to the data object to fill.
* @param int $expected_interval_count Number of expected intervals.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array( $selections = $this->selected_columns( $query_args );
'per_page' => get_option( 'posts_per_page' ), $this->add_sql_query_params( $query_args );
'page' => 1, $where_time = $this->add_time_period_sql_params( $query_args, $table_name );
'order' => 'DESC', $this->add_intervals_sql_params( $query_args, $table_name );
'orderby' => 'date',
'fields' => '*', $this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
'interval' => 'week', $this->interval_query->str_replace_clause( 'select', 'date_created', 'timestamp' );
'before' => TimeInterval::default_before(), $this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
'after' => TimeInterval::default_after(),
$db_intervals = $wpdb->get_col(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->interval_query->get_query_statement()
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $db_records_count = count( $db_intervals );
* 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->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name );
$this->initialize_queries(); $this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$selections = $this->selected_columns( $query_args ); $this->total_query->add_sql_clause( 'select', $selections );
$this->add_sql_query_params( $query_args ); $this->total_query->add_sql_clause( 'where', $this->interval_query->get_sql_clause( 'where' ) );
$where_time = $this->add_time_period_sql_params( $query_args, $table_name ); if ( $where_time ) {
$this->add_intervals_sql_params( $query_args, $table_name ); $this->total_query->add_sql_clause( 'where_time', $where_time );
}
$totals = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->total_query->get_query_statement(),
ARRAY_A
);
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce' ) );
}
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' ); $this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->str_replace_clause( 'select', 'date_created', 'timestamp' ); $this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' ); $this->interval_query->add_sql_clause( 'select', ', MAX(timestamp) AS datetime_anchor' );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$db_intervals = $wpdb->get_col( $intervals = $wpdb->get_results(
$this->interval_query->get_query_statement() // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok. $this->interval_query->get_query_statement(),
ARRAY_A
);
$db_records_count = count( $db_intervals ); if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce' ) );
}
$params = $this->get_limit_params( $query_args ); $totals = (object) $this->cast_numbers( $totals[0] );
$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();
}
$this->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name ); $data->totals = $totals;
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' ); $data->intervals = $intervals;
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'where', $this->interval_query->get_sql_clause( 'where' ) );
if ( $where_time ) {
$this->total_query->add_sql_clause( 'where_time', $where_time );
}
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads data failed.', 'woocommerce' ) );
}
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) ); if ( $this->intervals_missing( $expected_interval_count, $db_records_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) ); $this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->interval_query->add_sql_clause( 'select', ', MAX(timestamp) AS datetime_anchor' ); $this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
if ( '' !== $selections ) { $this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_records_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
$this->interval_query->add_sql_clause( 'select', ', ' . $selections ); } else {
} $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_downloads_stats_result_failed', __( 'Sorry, fetching downloads 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 ( $this->intervals_missing( $expected_interval_count, $db_records_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_records_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 );
}
$this->create_interval_subtotals( $data->intervals );
$this->set_cached_data( $cache_key, $data );
} }
return $data; return $data;
@ -163,6 +161,8 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
/** /**
* Normalizes order_by clause to match to SQL query. * Normalizes order_by clause to match to SQL query.
* *
* @override DownloadsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user. * @param string $order_by Order by option requeste by user.
* @return string * @return string
*/ */
@ -173,18 +173,4 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
return $order_by; 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' );
}
} }

View File

@ -11,12 +11,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Downloads\Stats\Query * API\Reports\Downloads\Stats\Query
*
* @deprecated 9.3.0 Downloads\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Orders report. * Valid fields for Orders report.
* *
* @deprecated 9.3.0 Downloads\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -26,6 +30,8 @@ class Query extends ReportsQuery {
/** /**
* Get revenue data based on the current query vars. * Get revenue data based on the current query vars.
* *
* @deprecated 9.3.0 Downloads\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -0,0 +1,58 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\API\Reports;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Trait to call filters on `get_data` methods for data stores.
*
* It calls the filters `woocommerce_analytics_{$this->context}_query_args` and
* `woocommerce_analytics_{$this->context}_select_query` on the `get_data` method.
*
* Example:
* <pre><code class="language-php">class MyStatsDataStore extends DataStore implements DataStoreInterface {
* // Use the trait.
* use FilteredGetDataTrait;
* // Provide all the necessary properties and methods for a regular DataStore.
* // ...
* }
* </code></pre>
*
* @see DataStore
*/
trait FilteredGetDataTrait {
/**
* Get the data based on args.
*
* Filters query args, calls DataStore::get_data, and returns the filtered data.
*
* @override ReportsDataStore::get_data()
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error
*/
public function get_data( $query_args ) {
/**
* Called before the data is fetched.
*
* @since 9.3.0
* @param array $query_args Query parameters.
*/
$args = apply_filters( "woocommerce_analytics_{$this->context}_query_args", $query_args );
$results = parent::get_data( $args );
/**
* Called after the data is fetched.
* The results can be modified here.
*
* @since 9.3.0
* @param stdClass|WP_Error $results The results of the query.
*/
return apply_filters( "woocommerce_analytics_{$this->context}_select_query", $results, $args );
}
}

View File

@ -7,10 +7,45 @@ use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
/** /**
* WC REST API Reports controller extended * {@see WC_REST_Reports_Controller WC REST API Reports Controller} extended to be shared as a generic base for all Analytics reports controllers.
* to be shared as a generic base for all Analytics controllers. *
* Handles pagination HTTP headers and links, basic, conventional params.
* Does all the REST API plumbing as `WC_REST_Controller`.
*
*
* Minimalistic example:
* <pre><code class="language-php">class MyController extends GenericController {
* /** Route of your new REST endpoint. &ast;/
* protected $rest_base = 'reports/my-thing';
* /**
* * Provide JSON schema for the response item.
* * @override WC_REST_Reports_Controller::get_item_schema()
* &ast;/
* public function get_item_schema() {
* $schema = array(
* '$schema' => 'http://json-schema.org/draft-04/schema#',
* 'title' => 'report_my_thing',
* 'type' => 'object',
* 'properties' => array(
* 'product_id' => array(
* 'type' => 'integer',
* 'readonly' => true,
* 'context' => array( 'view', 'edit' ),
* 'description' => __( 'Product ID.', 'my_extension' ),
* ),
* ),
* );
* // Add additional fields from `get_additional_fields` method and apply `woocommerce_rest_' . $schema['title'] . '_schema` filter.
* return $this->add_additional_fields_schema( $schema );
* }
* }
* </code></pre>
*
* The above Controller will get the data from a {@see DataStore data store} registered as `$rest_base` (`reports/my-thing`).
* (To change this behavior, override the `get_datastore_data()` method).
*
* To use the controller, please register it with the filter `woocommerce_admin_rest_controllers` filter.
* *
* @internal
* @extends WC_REST_Reports_Controller * @extends WC_REST_Reports_Controller
*/ */
abstract class GenericController extends \WC_REST_Reports_Controller { abstract class GenericController extends \WC_REST_Reports_Controller {
@ -26,12 +61,12 @@ abstract class GenericController extends \WC_REST_Reports_Controller {
/** /**
* Add pagination headers and links. * Add pagination headers and links.
* *
* @param WP_REST_Request $request Request data. * @param \WP_REST_Request $request Request data.
* @param WP_REST_Response|array $response Response data. * @param \WP_REST_Response|array $response Response data.
* @param int $total Total results. * @param int $total Total results.
* @param int $page Current page. * @param int $page Current page.
* @param int $max_pages Total amount of pages. * @param int $max_pages Total amount of pages.
* @return WP_REST_Response * @return \WP_REST_Response
*/ */
public function add_pagination_headers( $request, $response, int $total, int $page, int $max_pages ) { public function add_pagination_headers( $request, $response, int $total, int $page, int $max_pages ) {
$response = rest_ensure_response( $response ); $response = rest_ensure_response( $response );
@ -62,7 +97,19 @@ abstract class GenericController extends \WC_REST_Reports_Controller {
} }
/** /**
* Get the query params for collections. * Get data from `{$this->rest_base}` store, based on the given query vars.
*
* @throws Exception When the data store is not found {@see WC_Data_Store WC_Data_Store}.
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$data_store = \WC_Data_Store::load( $this->rest_base );
return $data_store->get_data( $query_args );
}
/**
* Get the query params definition for collections.
* *
* @return array * @return array
*/ */
@ -124,15 +171,62 @@ abstract class GenericController extends \WC_REST_Reports_Controller {
return $params; return $params;
} }
/** /**
* Prepare a report object for serialization. * Get the report data.
* *
* @param array $report Report data. * Prepares query params, fetches the report data from the Query object,
* @param WP_REST_Request $request Request object. * prepares it for the response, and packs it into the convention-conforming response object.
*
* @throws \WP_Error When the queried data is invalid.
* @param \WP_REST_Request $request Request data.
* @return \WP_Error|\WP_REST_Response
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$report_data = $this->get_datastore_data( $query_args );
if ( is_wp_error( $report_data ) ) {
return $report_data;
}
if ( ! isset( $report_data->data ) || ! isset( $report_data->page_no ) || ! isset( $report_data->pages ) ) {
return new \WP_Error( 'woocommerce_rest_reports_invalid_response', __( 'Invalid response from data store.', 'woocommerce' ), array( 'status' => 500 ) );
}
$out_data = array();
foreach ( $report_data->data as $datum ) {
$item = $this->prepare_item_for_response( $datum, $request );
$out_data[] = $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 data item for serialization.
*
* This method is called by `get_items` to prepare a single report data item for serialization.
* Calls `add_additional_fields_to_object` and `filter_response_by_context`,
* then wpraps the data with `rest_ensure_response`.
*
* You can extend it to add or filter some fields.
*
* @override WP_REST_Posts_Controller::prepare_item_for_response()
*
* @param mixed $report_item Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report_item, $request ) {
$data = $report; $data = $report_item;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->add_additional_fields_to_object( $data, $request );
@ -141,4 +235,26 @@ abstract class GenericController extends \WC_REST_Reports_Controller {
// Wrap the data in a response object. // Wrap the data in a response object.
return rest_ensure_response( $data ); return rest_ensure_response( $data );
} }
/**
* Maps query arguments from the REST request, to be fed to Query.
*
* `WP_REST_Request` does not expose a method to return all params covering defaults,
* as it does for `$request['param']` accessor.
* Therefore, we re-implement defaults resolution.
*
* @param \WP_REST_Request $request Full request object.
* @return array Simplified array of params.
*/
protected function prepare_reports_query( $request ) {
$args = wp_parse_args(
array_intersect_key(
$request->get_query_params(),
$this->get_collection_params()
),
$request->get_default_params()
);
return $args;
}
} }

View File

@ -0,0 +1,91 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
use WC_Data_Store;
/**
* A generic class for a report-specific query to be used in Analytics.
*
* Example usage:
* <pre><code class="language-php">$args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* );
* $report = new GenericQuery( $args, 'coupons' );
* $mydata = $report->get_data();
* </code></pre>
*
* It uses the name provided in the class property or in the constructor call to load the `report-{name}` data store.
*
* It's used by the {@see GenericController GenericController}.
*
* @since 9.3.0
*/
class GenericQuery extends \WC_Object_Query {
/**
* Specific query name.
* Will be used to load the `report-{name}` data store,
* and to call `woocommerce_analytics_{snake_case(name)}_*` filters.
*
* @var string
*/
protected $name;
/**
* Create a new query.
*
* @param array $args Criteria to query on in a format similar to WP_Query.
* @param string $name Query name.
* @extends WC_Object_Query::_construct
*/
public function __construct( $args, $name = null ) {
$this->name = $name ?? $this->name;
return parent::__construct( $args ); // phpcs:ignore Universal.CodeAnalysis.ConstructorDestructorReturn.ReturnValueFound
}
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get data from `report-{$name}` store, based on the current query vars.
* Filters query vars through `woocommerce_analytics_{snake_case(name)}_query_args` filter.
* Filters results through `woocommerce_analytics_{snake_case(name)}_select_query` filter.
*
* @return mixed filtered results from the data store.
*/
public function get_data() {
$snake_name = str_replace( '-', '_', $this->name );
/**
* Filter query args given for the report.
*
* @since 9.3.0
*
* @param array $query_args Query args.
*/
$args = apply_filters( "woocommerce_analytics_{$snake_name}_query_args", $this->get_query_vars() );
$data_store = \WC_Data_Store::load( "report-{$this->name}" );
$results = $data_store->get_data( $args );
/**
* Filter report query results.
*
* @since 9.3.0
*
* @param stdClass|WP_Error $results Results from the data store.
* @param array $args Query args used to get the data (potentially filtered).
*/
return apply_filters( "woocommerce_analytics_{$snake_name}_select_query", $results, $args );
}
}

View File

@ -6,21 +6,69 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController; use Automattic\WooCommerce\Admin\API\Reports\GenericController;
/** /**
* Generic base for all Stats controllers. * Generic base for all stats controllers.
*
* {@see GenericController Generic Controller} extended to be shared as a generic base for all Analytics stats controllers.
*
* Besides the `GenericController` functionality, it adds conventional stats-specific collection params and item schema.
* So, you may want to extend only your report-specific {@see get_item_properties_schema() get_item_properties_schema()}`.
* It also uses the stats-specific {@see get_items() get_items()} method,
* which packs report data into `totals` and `intervals`.
*
*
* Minimalistic example:
* <pre><code class="language-php">class StatsController extends GenericStatsController {
* /** Route of your new REST endpoint. &ast;/
* protected $rest_base = 'reports/my-thing/stats';
* /** Define your proeprties schema. &ast;/
* protected function get_item_properties_schema() {
* return array(
* 'my_property' => array(
* 'title' => __( 'My property', 'my-extension' ),
* 'type' => 'integer',
* 'readonly' => true,
* 'context' => array( 'view', 'edit' ),
* 'description' => __( 'Amazing thing.', 'my-extension' ),
* 'indicator' => true,
* ),
* );
* }
* /** Define overall schema. You can use the defaults,
* * just remember to provide your title and call `add_additional_fields_schema`
* * to run the filters
* &ast;/
* public function get_item_schema() {
* $schema = parent::get_item_schema();
* $schema['title'] = 'report_my_thing_stats';
*
* return $this->add_additional_fields_schema( $schema );
* }
* }
* </code></pre>
* *
* @internal
* @extends GenericController * @extends GenericController
*/ */
abstract class GenericStatsController extends GenericController { abstract class GenericStatsController extends GenericController {
/** /**
* Get the query params for collections. * Get the query params definition for collections.
* Adds intervals to the generic list. * Adds `fields` & `intervals` to the generic list.
*
* @override GenericController::get_collection_params()
* *
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$params = parent::get_collection_params(); $params = parent::get_collection_params();
$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['interval'] = array( $params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ), 'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string', 'type' => 'string',
@ -40,7 +88,7 @@ abstract class GenericStatsController extends GenericController {
} }
/** /**
* Get the Report's item properties schema. * Get the report's item properties schema.
* Will be used by `get_item_schema` as `totals` and `subtotals`. * Will be used by `get_item_schema` as `totals` and `subtotals`.
* *
* @return array * @return array
@ -50,7 +98,7 @@ abstract class GenericStatsController extends GenericController {
/** /**
* Get the Report's schema, conforming to JSON Schema. * Get the Report's schema, conforming to JSON Schema.
* *
* Please note, it does not call add_additional_fields_schema, * Please note that it does not call add_additional_fields_schema,
* as you may want to update the `title` first. * as you may want to update the `title` first.
* *
* @return array * @return array
@ -155,4 +203,43 @@ abstract class GenericStatsController extends GenericController {
), ),
); );
} }
/**
* Get the report data.
*
* Prepares query params, fetches the report data from the Query object,
* prepares it for the response, and packs it into the convention-conforming response object.
*
* @override GenericController::get_items()
*
* @throws \WP_Error When the queried data is invalid.
* @param \WP_REST_Request $request Request data.
* @return \WP_REST_Response|\WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
try {
$report_data = $this->get_datastore_data( $query_args );
} catch ( ParameterException $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array(
'totals' => $report_data->totals ? get_object_vars( $report_data->totals ) : null,
'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
);
}
} }

View File

@ -0,0 +1,123 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\API\Reports;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Trait to contain shared methods for reports Controllers that use order and orders statuses.
*
* If your analytics controller needs to work with orders,
* you will most probably need to use at least {@see get_order_statuses() get_order_statuses()}
* to filter only "actionable" statuses to produce consistent results among other analytics.
*
* @see GenericController
*/
trait OrderAwareControllerTrait {
/**
* Get the order number for an order. If no filter is present for `woocommerce_order_number`, we can just return the ID.
* Returns the parent order number if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string|null The Order Number or null if the order doesn't exist.
*/
protected function get_order_number( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
// If the parent order doesn't exist, return null.
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
}
if ( ! has_filter( 'woocommerce_order_number' ) ) {
return $order->get_id();
}
return $order->get_order_number();
}
/**
* Whether the order is valid.
*
* @param bool|WC_Order|WC_Order_Refund $order Order object.
* @return bool True if the order is valid, false otherwise.
*/
protected function is_valid_order( $order ) {
return $order instanceof \WC_Order || $order instanceof \WC_Order_Refund;
}
/**
* Get the order total with the related currency formatting.
* Returns the parent order total if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string|null The Order Number or null if the order doesn't exist.
*/
protected function get_total_formatted( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
}
return wp_strip_all_tags( html_entity_decode( $order->get_formatted_order_total() ), true );
}
/**
* Get order statuses without prefixes.
* Includes unregistered statuses that have been marked "actionable".
*
* @return array
*/
public static function get_order_statuses() {
// Allow all statuses selected as "actionable" - this may include unregistered statuses.
// See: https://github.com/woocommerce/woocommerce-admin/issues/5592.
$actionable_statuses = get_option( 'woocommerce_actionable_order_statuses', array() );
// See WC_REST_Orders_V2_Controller::get_collection_params() re: any/trash statuses.
$registered_statuses = array_merge( array( 'any', 'trash' ), array_keys( self::get_order_status_labels() ) );
// Merge the status arrays (using flip to avoid array_unique()).
$allowed_statuses = array_keys( array_merge( array_flip( $registered_statuses ), array_flip( $actionable_statuses ) ) );
return $allowed_statuses;
}
/**
* Get order statuses (and labels) without prefixes.
*
* @internal
* @return array
*/
public static function get_order_status_labels() {
$order_statuses = array();
foreach ( wc_get_order_statuses() as $key => $label ) {
$new_key = str_replace( 'wc-', '', $key );
$order_statuses[ $new_key ] = $label;
}
return $order_statuses;
}
}

View File

@ -9,16 +9,19 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
defined( 'ABSPATH' ) || exit; 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\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/** /**
* REST API Reports orders controller class. * REST API Reports orders controller class.
* *
* @internal * @internal
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller * @extends \Automattic\WooCommerce\Admin\API\Reports\GenericController
*/ */
class Controller extends ReportsController implements ExportableInterface { class Controller extends GenericController implements ExportableInterface {
use OrderAwareControllerTrait;
/** /**
* Route base. * Route base.
@ -27,6 +30,19 @@ class Controller extends ReportsController implements ExportableInterface {
*/ */
protected $rest_base = 'reports/orders'; protected $rest_base = 'reports/orders';
/**
* Get data from Query.
*
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$query = new Query( $query_args );
return $query->get_data();
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -65,50 +81,17 @@ class Controller extends ReportsController implements ExportableInterface {
} }
/** /**
* Get all reports. * Prepare a report data item for serialization.
* *
* @param WP_REST_Request $request Request data. * @param array $report Report data item as returned from Data Store.
* @return array|WP_Error * @param \WP_REST_Request $request Request object.
*/ * @return \WP_REST_Response
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$orders_query = new Query( $query_args );
$report_data = $orders_query->get_data();
$data = array();
foreach ( $report_data->data as $orders_data ) {
$orders_data['order_number'] = $this->get_order_number( $orders_data['order_id'] );
$orders_data['total_formatted'] = $this->get_total_formatted( $orders_data['order_id'] );
$item = $this->prepare_item_for_response( $orders_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$data,
(int) $report_data->total,
(int) $report_data->page_no,
(int) $report_data->pages
);
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report, $request ) {
$data = $report; $report['order_number'] = $this->get_order_number( $report['order_id'] );
$report['total_formatted'] = $this->get_total_formatted( $report['order_id'] );
$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. // Wrap the data in a response object.
$response = rest_ensure_response( $data ); $response = parent::prepare_item_for_response( $report, $request );
$response->add_links( $this->prepare_links( $report ) ); $response->add_links( $this->prepare_links( $report ) );
/** /**
@ -248,54 +231,12 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$params = array(); $params = parent::get_collection_params();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); $params['per_page']['minimum'] = 0;
$params['page'] = array( $params['orderby']['enum'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ), 'date',
'type' => 'integer', 'num_items_sold',
'default' => 1, 'net_total',
'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' => 0,
'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['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',
'num_items_sold',
'net_total',
),
'validate_callback' => 'rest_validate_request_arg',
); );
$params['product_includes'] = array( $params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
@ -464,12 +405,6 @@ class Controller extends ReportsController implements ExportableInterface {
'default' => array(), 'default' => array(),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$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; return $params;
} }

View File

@ -14,7 +14,6 @@ use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use Automattic\WooCommerce\Admin\API\Reports\Cache; use Automattic\WooCommerce\Admin\API\Reports\Cache;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/** /**
@ -25,6 +24,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Dynamically sets the date column name based on configuration * Dynamically sets the date column name based on configuration
*
* @override ReportsDataStore::__construct()
*/ */
public function __construct() { public function __construct() {
$this->date_column_name = get_option( 'woocommerce_date_type', 'date_paid' ); $this->date_column_name = get_option( 'woocommerce_date_type', 'date_paid' );
@ -34,6 +35,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_stats'; protected static $table_name = 'wc_order_stats';
@ -41,6 +44,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'orders'; protected $cache_key = 'orders';
@ -48,6 +53,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -66,12 +73,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'orders'; protected $context = 'orders';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -213,117 +224,118 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = array_merge(
parent::get_default_query_vars(),
array(
'orderby' => $this->date_column_name,
'product_includes' => array(),
'product_excludes' => array(),
'coupon_includes' => array(),
'coupon_excludes' => array(),
'tax_rate_includes' => array(),
'tax_rate_excludes' => array(),
'customer_type' => null,
'status_is' => array(),
'extended_info' => false,
'refunds' => null,
'order_includes' => array(),
'order_excludes' => array(),
)
);
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $data = (object) array(
'page' => 1, 'data' => array(),
'order' => 'DESC', 'total' => 0,
'orderby' => $this->date_column_name, 'pages' => 0,
'before' => TimeInterval::default_before(), 'page_no' => 0,
'after' => TimeInterval::default_after(),
'fields' => '*',
'product_includes' => array(),
'product_excludes' => array(),
'coupon_includes' => array(),
'coupon_excludes' => array(),
'tax_rate_includes' => array(),
'tax_rate_excludes' => array(),
'customer_type' => null,
'status_is' => array(),
'extended_info' => false,
'refunds' => null,
'order_includes' => array(),
'order_excludes' => array(),
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $selections = $this->selected_columns( $query_args );
* We need to get the cache key here because $params = $this->get_limit_params( $query_args );
* parent::update_intervals_sql_params() modifies $query_args. $this->add_sql_query_params( $query_args );
*/ /* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$cache_key = $this->get_cache_key( $query_args ); $db_records_count = (int) $wpdb->get_var(
$data = $this->get_cached_data( $cache_key ); "SELECT COUNT( DISTINCT tt.order_id ) FROM (
{$this->subquery->get_query_statement()}
if ( false === $data ) { ) AS tt"
$this->initialize_queries(); );
/* phpcs:enable */
if ( 0 === $params['per_page'] ) {
$total_pages = 0;
} else {
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
}
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
$data = (object) array( $data = (object) array(
'data' => array(), 'data' => array(),
'total' => 0, 'total' => $db_records_count,
'pages' => 0, 'pages' => 0,
'page_no' => 0, 'page_no' => 0,
); );
return $data;
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT( DISTINCT tt.order_id ) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
);
/* phpcs:enable */
if ( 0 === $params['per_page'] ) {
$total_pages = 0;
} else {
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
}
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
$data = (object) array(
'data' => array(),
'total' => $db_records_count,
'pages' => 0,
'page_no' => 0,
);
return $data;
}
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$orders_data = $wpdb->get_results(
$this->subquery->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
if ( null === $orders_data ) {
return $data;
}
if ( $query_args['extended_info'] ) {
$this->include_extended_info( $orders_data, $query_args );
}
$orders_data = array_map( array( $this, 'cast_numbers' ), $orders_data );
$data = (object) array(
'data' => $orders_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
} }
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$orders_data = $wpdb->get_results(
$this->subquery->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
if ( null === $orders_data ) {
return $data;
}
if ( $query_args['extended_info'] ) {
$this->include_extended_info( $orders_data, $query_args );
}
$orders_data = array_map( array( $this, 'cast_numbers' ), $orders_data );
$data = (object) array(
'data' => $orders_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
return $data; return $data;
} }
/** /**
* Normalizes order_by clause to match to SQL query. * Normalizes order_by clause to match to SQL query.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user. * @param string $order_by Order by option requeste by user.
* @return string * @return string
*/ */

View File

@ -19,24 +19,32 @@
namespace Automattic\WooCommerce\Admin\API\Reports\Orders; namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Orders\Query * API\Reports\Orders\Query
*/ */
class Query extends ReportsQuery { class Query extends GenericQuery {
/** /**
* Get order data based on the current query vars. * Specific query name.
* Will be used to load the `report-{name}` data store,
* and to call `woocommerce_analytics_{snake_case(name)}_*` filters.
*
* @var string
*/
protected $name = 'orders';
/**
* Get the default allowed query vars.
* *
* @return array * @return array
*/ */
public function get_data() { protected function get_default_query_vars() {
$args = apply_filters( 'woocommerce_analytics_orders_query_args', $this->get_query_vars() ); return \WC_Object_Query::get_default_query_vars();
$data_store = \WC_Data_Store::load( 'report-orders' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_orders_select_query', $results, $args );
} }
} }

View File

@ -9,15 +9,19 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException; use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query;
/** /**
* REST API Reports orders stats controller class. * REST API Reports orders stats controller class.
* *
* @internal * @internal
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller * @extends \Automattic\WooCommerce\Admin\API\Reports\GenericStatsController
*/ */
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller { class Controller extends GenericStatsController {
use OrderAwareControllerTrait;
/** /**
* Route base. * Route base.
@ -26,6 +30,19 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
*/ */
protected $rest_base = 'reports/orders/stats'; protected $rest_base = 'reports/orders/stats';
/**
* Get data from Query.
*
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$query = new Query( $query_args );
return $query->get_data();
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -70,55 +87,15 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
} }
/** /**
* Get all reports. * Prepare a report data item for serialization.
* *
* @param WP_REST_Request $request Request data. * @param Array $report Report data item as returned from Data Store.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$orders_query = new Query( $query_args );
try {
$report_data = $orders_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. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { 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. // Wrap the data in a response object.
$response = rest_ensure_response( $data ); $response = parent::prepare_item_for_response( $report, $request );
/** /**
* Filter a report returned from the API. * Filter a report returned from the API.
@ -132,13 +109,15 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
return apply_filters( 'woocommerce_rest_prepare_report_orders_stats', $response, $report, $request ); return apply_filters( 'woocommerce_rest_prepare_report_orders_stats', $response, $report, $request );
} }
/** /**
* Get the Report's schema, conforming to JSON Schema. * Get the Report's item properties schema.
* Will be used by `get_item_schema` as `totals` and `subtotals`.
* *
* @return array * @return array
*/ */
public function get_item_schema() { protected function get_item_properties_schema() {
$data_values = array( return array(
'net_revenue' => array( 'net_revenue' => array(
'description' => __( 'Net sales.', 'woocommerce' ), 'description' => __( 'Net sales.', 'woocommerce' ),
'type' => 'number', 'type' => 'number',
@ -199,104 +178,19 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'readonly' => true, 'readonly' => true,
), ),
); );
}
$segments = array( /**
'segments' => array( * Get the Report's schema, conforming to JSON Schema.
'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ), *
'type' => 'array', * @return array
'context' => array( 'view', 'edit' ), */
'readonly' => true, public function get_item_schema() {
'items' => array( $schema = parent::get_item_schema();
'type' => 'object', $schema['title'] = 'report_orders_stats';
'properties' => array(
'segment_id' => array(
'description' => __( 'Segment identificator.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $data_values,
),
),
),
),
);
$totals = array_merge( $data_values, $segments );
// Products is not shown in intervals. // Products is not shown in intervals.
unset( $data_values['products'] ); unset( $schema['properties']['intervals']['items']['properties']['subtotals']['properties']['products'] );
$intervals = array_merge( $data_values, $segments );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_orders_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $intervals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema ); return $this->add_additional_fields_schema( $schema );
} }
@ -307,69 +201,12 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$params = array(); $params = parent::get_collection_params();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); $params['orderby']['enum'] = array(
$params['page'] = array( 'date',
'description' => __( 'Current page of the collection.', 'woocommerce' ), 'net_revenue',
'type' => 'integer', 'orders_count',
'default' => 1, 'avg_order_value',
'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['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',
'avg_order_value',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
); );
$params['match'] = array( $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' ), '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' ),
@ -412,7 +249,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['product_excludes'] = array( $params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -421,7 +258,8 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(), 'default' => array(),
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['variation_includes'] = array( // Split assignments for PHPCS complaining on aligned.
$params['variation_includes'] = array(
'description' => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -431,7 +269,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['variation_excludes'] = array( $params['variation_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -441,7 +279,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['coupon_includes'] = array( $params['coupon_includes'] = array(
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -450,7 +288,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(), 'default' => array(),
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['coupon_excludes'] = array( $params['coupon_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -459,7 +297,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(), 'default' => array(),
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['tax_rate_includes'] = array( $params['tax_rate_includes'] = array(
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -469,7 +307,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['tax_rate_excludes'] = array( $params['tax_rate_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce' ), 'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -479,7 +317,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['customer'] = array( $params['customer'] = array(
'description' => __( 'Alias for customer_type (deprecated).', 'woocommerce' ), 'description' => __( 'Alias for customer_type (deprecated).', 'woocommerce' ),
'type' => 'string', 'type' => 'string',
'enum' => array( 'enum' => array(
@ -488,7 +326,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['customer_type'] = array( $params['customer_type'] = array(
'description' => __( 'Limit result set to orders that have the specified customer_type', 'woocommerce' ), 'description' => __( 'Limit result set to orders that have the specified customer_type', 'woocommerce' ),
'type' => 'string', 'type' => 'string',
'enum' => array( 'enum' => array(
@ -497,7 +335,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['refunds'] = array( $params['refunds'] = array(
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce' ), 'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce' ),
'type' => 'string', 'type' => 'string',
'default' => '', 'default' => '',
@ -510,7 +348,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['attribute_is'] = array( $params['attribute_is'] = array(
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ), 'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -519,7 +357,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(), 'default' => array(),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['attribute_is_not'] = array( $params['attribute_is_not'] = array(
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ), 'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -528,7 +366,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(), 'default' => array(),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['segmentby'] = array( $params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'woocommerce' ), 'description' => __( 'Segment the response by additional constraint.', 'woocommerce' ),
'type' => 'string', 'type' => 'string',
'enum' => array( 'enum' => array(
@ -540,21 +378,8 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['fields'] = array( unset( $params['intervals'] );
'description' => __( 'Limit stats fields to the specified items.', 'woocommerce' ), unset( $params['fields'] );
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
$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; return $params;
} }

View File

@ -14,15 +14,19 @@ use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore; use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil; use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/** /**
* API\Reports\Orders\Stats\DataStore. * API\Reports\Orders\Stats\DataStore.
*/ */
class DataStore extends ReportsDataStore implements DataStoreInterface { class DataStore extends ReportsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_stats'; protected static $table_name = 'wc_order_stats';
@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'orders_stats'; protected $cache_key = 'orders_stats';
@ -42,6 +48,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Type for each column to cast values correctly later. * Type for each column to cast values correctly later.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -65,12 +73,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'orders_stats'; protected $context = 'orders_stats';
/** /**
* Dynamically sets the date column name based on configuration * Dynamically sets the date column name based on configuration
*
* @override ReportsDataStore::__construct()
*/ */
public function __construct() { public function __construct() {
$this->date_column_name = get_option( 'woocommerce_date_type', 'date_paid' ); $this->date_column_name = get_option( 'woocommerce_date_type', 'date_paid' );
@ -79,6 +91,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -260,176 +274,161 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = array_merge(
parent::get_default_query_vars(),
array(
'interval' => 'week',
'segmentby' => '',
'match' => 'all',
'status_is' => array(),
'status_is_not' => array(),
'product_includes' => array(),
'product_excludes' => array(),
'coupon_includes' => array(),
'coupon_excludes' => array(),
'tax_rate_includes' => array(),
'tax_rate_excludes' => array(),
'customer_type' => '',
'category_includes' => array(),
)
);
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_stats_data()
*
* @see get_data
* @see get_noncached_stats_data
* @param array $query_args Query parameters.
* @param array $params Query limit parameters.
* @param stdClass $data Reference to the data object to fill.
* @param int $expected_interval_count Number of expected intervals.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'interval' => 'week',
'fields' => '*',
'segmentby' => '',
'match' => 'all',
'status_is' => array(),
'status_is_not' => array(),
'product_includes' => array(),
'product_excludes' => array(),
'coupon_includes' => array(),
'coupon_excludes' => array(),
'tax_rate_includes' => array(),
'tax_rate_excludes' => array(),
'customer_type' => '',
'category_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 ( isset( $query_args['date_type'] ) ) { if ( isset( $query_args['date_type'] ) ) {
$this->date_column_name = $query_args['date_type']; $this->date_column_name = $query_args['date_type'];
} }
if ( false === $data ) { $this->initialize_queries();
$this->initialize_queries();
$data = (object) array( $selections = $this->selected_columns( $query_args );
'totals' => (object) array(), $this->add_time_period_sql_params( $query_args, $table_name );
'intervals' => (object) array(), $this->add_intervals_sql_params( $query_args, $table_name );
'total' => 0, $this->add_order_by_sql_params( $query_args );
'pages' => 0, $where_time = $this->get_sql_clause( 'where_time' );
'page_no' => 0, $params = $this->get_limit_sql_params( $query_args );
); $coupon_join = "LEFT JOIN (
SELECT
order_id,
SUM(discount_amount) AS discount_amount,
COUNT(DISTINCT coupon_id) AS coupons_count
FROM
{$wpdb->prefix}wc_order_coupon_lookup
GROUP BY
order_id
) order_coupon_lookup
ON order_coupon_lookup.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$selections = $this->selected_columns( $query_args ); // Additional filtering for Orders report.
$this->add_time_period_sql_params( $query_args, $table_name ); $this->orders_stats_sql_filter( $query_args );
$this->add_intervals_sql_params( $query_args, $table_name ); $this->total_query->add_sql_clause( 'select', $selections );
$this->add_order_by_sql_params( $query_args ); $this->total_query->add_sql_clause( 'left_join', $coupon_join );
$where_time = $this->get_sql_clause( 'where_time' ); $this->total_query->add_sql_clause( 'where_time', $where_time );
$params = $this->get_limit_sql_params( $query_args ); $totals = $wpdb->get_results(
$coupon_join = "LEFT JOIN ( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
SELECT $this->total_query->get_query_statement(),
order_id, ARRAY_A
SUM(discount_amount) AS discount_amount, );
COUNT(DISTINCT coupon_id) AS coupons_count if ( null === $totals ) {
FROM return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
{$wpdb->prefix}wc_order_coupon_lookup
GROUP BY
order_id
) order_coupon_lookup
ON order_coupon_lookup.order_id = {$wpdb->prefix}wc_order_stats.order_id";
// Additional filtering for Orders report.
$this->orders_stats_sql_filter( $query_args );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'left_join', $coupon_join );
$this->total_query->add_sql_clause( 'where_time', $where_time );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
// phpcs:ignore Generic.Commenting.Todo.TaskFound
// @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' => $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' => $where_time,
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['products'] = $unique_products;
$segmenter = new Segmenter( $query_args, $this->report_columns );
$unique_coupons = $this->get_unique_coupon_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['coupons_count'] = $unique_coupons;
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->add_sql_clause( 'left_join', $coupon_join );
$this->interval_query->add_sql_clause( 'where_time', $where_time );
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // phpcs:ignore cache ok, DB call ok, , unprepared SQL ok.
$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 $data;
}
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$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 );
}
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
if ( isset( $intervals[0] ) ) {
$unique_coupons = $this->get_unique_coupon_count( $intervals_query['from_clause'], $intervals_query['where_time_clause'], $intervals_query['where_clause'], true );
$intervals[0]['coupons_count'] = $unique_coupons;
}
$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 );
} }
// phpcs:ignore Generic.Commenting.Todo.TaskFound
// @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' => $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' => $where_time,
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['products'] = $unique_products;
$segmenter = new Segmenter( $query_args, $this->report_columns );
$unique_coupons = $this->get_unique_coupon_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] );
$totals[0]['coupons_count'] = $unique_coupons;
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$totals = (object) $this->cast_numbers( $totals[0] );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->add_sql_clause( 'left_join', $coupon_join );
$this->interval_query->add_sql_clause( 'where_time', $where_time );
$db_intervals = $wpdb->get_col(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, , unprepared SQL ok.
$this->interval_query->get_query_statement()
);
$db_interval_count = count( $db_intervals );
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$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 );
}
$intervals = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, , unprepared SQL ok.
$this->interval_query->get_query_statement(),
ARRAY_A
);
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
if ( isset( $intervals[0] ) ) {
$unique_coupons = $this->get_unique_coupon_count( $intervals_query['from_clause'], $intervals_query['where_time_clause'], $intervals_query['where_clause'], true );
$intervals[0]['coupons_count'] = $unique_coupons;
}
$data->totals = $totals;
$data->intervals = $intervals;
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 );
return $data; return $data;
} }
@ -729,18 +728,4 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
) )
); );
} }
/**
* 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' );
}
} }

View File

@ -17,14 +17,23 @@
namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats; namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery; defined( 'ABSPATH' ) || exit;
/** /**
* API\Reports\Orders\Stats\Query * API\Reports\Orders\Stats\Query
*/ */
class Query extends ReportsQuery { class Query extends GenericQuery {
/**
* Specific query name.
* Will be used to load the `report-{name}` data store,
* and to call `woocommerce_analytics_{snake_case(name)}_*` filters.
*
* @var string
*/
protected $name = 'orders-stats';
/** /**
* Valid fields for Orders report. * Valid fields for Orders report.
@ -45,17 +54,4 @@ class Query extends ReportsQuery {
), ),
); );
} }
/**
* Get revenue data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_orders_stats_query_args', $this->get_query_vars() );
$data_store = \WC_Data_Store::load( 'report-orders-stats' );
$results = $data_store->get_data( $args );
return apply_filters( 'woocommerce_analytics_orders_stats_select_query', $results, $args );
}
} }

View File

@ -452,10 +452,10 @@ class Controller extends GenericController {
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param array $stat_data Report data. * @param array $stat_data Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $stat_data, $request ) { public function prepare_item_for_response( $stat_data, $request ) {

View File

@ -9,8 +9,9 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface; use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -41,51 +42,22 @@ class Controller extends GenericController implements ExportableInterface {
); );
/** /**
* Get items. * Get data from `'products'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* *
* @return array|WP_Error * @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$args = array(); $query = new GenericQuery( $query_args, 'products' );
$registered = array_keys( $this->get_collection_params() ); return $query->get_data();
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 );
if ( isset( $item->data['extended_info']['name'] ) ) {
$item->data['extended_info']['name'] = wp_strip_all_tags( $item->data['extended_info']['name'] );
}
$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. * Prepare a report data item for serialization.
* *
* @param Array $report Report data. * @param Array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
@ -101,8 +73,36 @@ class Controller extends GenericController implements ExportableInterface {
* @param WP_REST_Response $response The response object. * @param WP_REST_Response $response The response object.
* @param object $report The original report object. * @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response. * @param WP_REST_Request $request Request used to generate the response.
*
* @since 6.5.0
*/ */
return apply_filters( 'woocommerce_rest_prepare_report_products', $response, $report, $request ); $filtered_response = apply_filters( 'woocommerce_rest_prepare_report_products', $response, $report, $request );
if ( isset( $filtered_response->data['extended_info']['name'] ) ) {
$filtered_response->data['extended_info']['name'] = wp_strip_all_tags( $filtered_response->data['extended_info']['name'] );
}
return $filtered_response;
}
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$registered = array_keys( $this->get_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 ];
}
}
}
return $args;
} }
/** /**

View File

@ -21,6 +21,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_product_lookup'; protected static $table_name = 'wc_order_product_lookup';
@ -28,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'products'; protected $cache_key = 'products';
@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -79,12 +85,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'products'; protected $context = 'products';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -175,6 +185,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* @return string * @return string
*/ */
@ -256,122 +268,137 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Returns the report data based on parameters supplied by the user. * Returns the report data based on parameters supplied by the user.
* *
* @override ReportsDataStore::get_data()
*
* @param array $query_args Query parameters. * @param array $query_args Query parameters.
* @return stdClass|WP_Error Data. * @return stdClass|WP_Error Data.
*/ */
public function get_data( $query_args ) { public function get_data( $query_args ) {
$data = parent::get_data( $query_args );
/*
* Do not cache extended info -- this is required to get the latest stock data.
* `include_extended_info` checks only `extended_info` key,
* so we don't need to bother about normalizing timestamps.
*/
$defaults = $this->get_default_query_vars();
$query_args = wp_parse_args( $query_args, $defaults );
$this->include_extended_info( $data->data, $query_args );
return $data;
}
/**
* Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
*
* @override ReportsDataStore::get_default_query_vars()
*
* @return array Query parameters.
*/
public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['category_includes'] = array();
$defaults['product_includes'] = array();
$defaults['extended_info'] = false;
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $data = (object) array(
'page' => 1, 'data' => array(),
'order' => 'DESC', 'total' => 0,
'orderby' => 'date', 'pages' => 0,
'before' => TimeInterval::default_before(), 'page_no' => 0,
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'product_includes' => array(),
'extended_info' => false,
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $selections = $this->selected_columns( $query_args );
* We need to get the cache key here because $included_products = $this->get_included_products_array( $query_args );
* parent::update_intervals_sql_params() modifies $query_args. $params = $this->get_limit_params( $query_args );
*/ $this->add_sql_query_params( $query_args );
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) { if ( count( $included_products ) > 0 ) {
$this->initialize_queries(); $filtered_products = array_diff( $included_products, array( '-1' ) );
$total_results = count( $filtered_products );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$data = (object) array( if ( 'date' === $query_args['orderby'] ) {
'data' => array(), $selections .= ", {$table_name}.date_created";
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$included_products = $this->get_included_products_array( $query_args );
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
if ( count( $included_products ) > 0 ) {
$filtered_products = array_diff( $included_products, array( '-1' ) );
$total_results = count( $filtered_products );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
if ( 'date' === $query_args['orderby'] ) {
$selections .= ", {$table_name}.date_created";
}
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'product_id' ) );
$ids_table = $this->get_ids_table( $included_products, 'product_id' );
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$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.product_id = {$table_name}.product_id"
);
$this->add_sql_clause( 'where', 'AND default_results.product_id != -1' );
$products_query = $this->get_query_statement();
} else {
$count_query = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt";
$db_records_count = (int) $wpdb->get_var(
$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
);
$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->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$products_query = $this->subquery->get_query_statement();
} }
$product_data = $wpdb->get_results( $fields = $this->get_fields( $query_args );
$products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $join_selections = $this->format_join_selections( $fields, array( 'product_id' ) );
ARRAY_A $ids_table = $this->get_ids_table( $included_products, 'product_id' );
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$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.product_id = {$table_name}.product_id"
);
$this->add_sql_clause( 'where', 'AND default_results.product_id != -1' );
$products_query = $this->get_query_statement();
} else {
$count_query = "SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt";
$db_records_count = (int) $wpdb->get_var(
$count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
); );
if ( null === $product_data ) { $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; return $data;
} }
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data ); $this->subquery->clear_sql_clause( 'select' );
$data = (object) array( $this->subquery->add_sql_clause( 'select', $selections );
'data' => $product_data, $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
'total' => $total_results, $this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
'pages' => $total_pages, $products_query = $this->subquery->get_query_statement();
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
} }
$this->include_extended_info( $data->data, $query_args ); $product_data = $wpdb->get_results(
$products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
if ( null === $product_data ) {
return $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'],
);
return $data; return $data;
} }

View File

@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Products\Query * API\Reports\Products\Query
*
* @deprecated 9.3.0 Products\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Products report. * Valid fields for Products report.
* *
* @deprecated 9.3.0 Products\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Products\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,8 +9,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController; use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -48,12 +48,25 @@ class Controller extends GenericStatsController {
} }
/** /**
* Get all reports. * Get data from `'products-stats'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return array|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'products-stats' );
return $query->get_data();
}
/**
* Maps query arguments from the REST request, to be fed to Query.
*
* @param \WP_REST_Request $request Full request object.
* @return array Simplified array of params.
*/
protected function prepare_reports_query( $request ) {
$query_args = array( $query_args = array(
'fields' => array( 'fields' => array(
'items_sold', 'items_sold',
@ -75,36 +88,13 @@ class Controller extends GenericStatsController {
} }
} }
$query = new Query( $query_args ); return $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. * Prepare a report data item for serialization.
* *
* @param array $report Report data. * @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
@ -255,15 +245,6 @@ class Controller extends GenericStatsController {
), ),
'validate_callback' => 'rest_validate_request_arg', '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',
),
);
return $params; return $params;
} }

View File

@ -8,18 +8,22 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/** /**
* API\Reports\Products\Stats\DataStore. * API\Reports\Products\Stats\DataStore.
*/ */
class DataStore extends ProductsDataStore implements DataStoreInterface { class DataStore extends ProductsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ProductsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -36,6 +40,8 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ProductsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'products_stats'; protected $cache_key = 'products_stats';
@ -43,12 +49,16 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ProductsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'products_stats'; protected $context = 'products_stats';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ProductsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -99,138 +109,141 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' ); $this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
} }
/**
* Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
*
* @override ProductsDataStore::get_default_query_vars()
*
* @return array Query parameters.
*/
public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['interval'] = 'week';
unset( $defaults['extended_info'] );
return $defaults;
}
/** /**
* Returns the report data based on parameters supplied by the user. * Returns the report data based on parameters supplied by the user.
* *
* @since 3.5.0 * @override ProductsDataStore::get_data()
*
* @param array $query_args Query parameters. * @param array $query_args Query parameters.
* @return stdClass|WP_Error Data. * @return stdClass|WP_Error Data.
*/ */
public function get_data( $query_args ) { public function get_data( $query_args ) {
// Do not include extended info like `ProductsDataStore` does.
return ReportsDataStore::get_data( $query_args );
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ProductsDataStore::get_noncached_data()
*
* @see get_data
* @see get_noncached_stats_data
* @param array $query_args Query parameters.
* @param array $params Query limit parameters.
* @param stdClass $data Reference to the data object to fill.
* @param int $expected_interval_count Number of expected intervals.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $selections = $this->selected_columns( $query_args );
'page' => 1,
'order' => 'DESC', $this->update_sql_query_params( $query_args );
'orderby' => 'date', $this->get_limit_sql_params( $query_args );
'before' => TimeInterval::default_before(), $this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
'after' => TimeInterval::default_after(),
'fields' => '*', $db_intervals = $wpdb->get_col(
'category_includes' => array(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
'interval' => 'week', $this->interval_query->get_query_statement()
'product_includes' => array(),
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $db_interval_count = count( $db_intervals );
* 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 ) { $intervals = array();
$this->initialize_queries(); $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' ) );
$selections = $this->selected_columns( $query_args ); $totals = $wpdb->get_results(
$params = $this->get_limit_params( $query_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->total_query->get_query_statement(),
ARRAY_A
);
$this->update_sql_query_params( $query_args ); // phpcs:ignore Generic.Commenting.Todo.TaskFound
$this->get_limit_sql_params( $query_args ); // @todo remove these assignements when refactoring segmenter classes to use query objects.
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) ); $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 );
$db_intervals = $wpdb->get_col( if ( null === $totals ) {
$this->interval_query->get_query_statement() return new \WP_Error( 'woocommerce_analytics_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$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' ) );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
// @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_products_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 );
}
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_products_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 );
} }
$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 );
}
$intervals = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->interval_query->get_query_statement(),
ARRAY_A
);
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$totals = (object) $this->cast_numbers( $totals[0] );
$data->totals = $totals;
$data->intervals = $intervals;
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 );
return $data; return $data;
} }
/** /**
* Normalizes order_by clause to match to SQL query. * Normalizes order_by clause to match to SQL query.
* *
* @override ProductsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user. * @param string $order_by Order by option requeste by user.
* @return string * @return string
*/ */
@ -241,18 +254,4 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
return $order_by; 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' );
}
} }

View File

@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Products\Stats\Query * API\Reports\Products\Stats\Query
*
* @deprecated 9.3.0 Products\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Products report. * Valid fields for Products report.
* *
* @deprecated 9.3.0 Products\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Products\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,12 +9,15 @@ defined( 'ABSPATH' ) || exit;
/** /**
* Admin\API\Reports\Query * Admin\API\Reports\Query
*
* @deprecated 9.3.0 Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
abstract class Query extends \WC_Object_Query { abstract class Query extends \WC_Object_Query {
/** /**
* Get report data matching the current query vars. * Get report data matching the current query vars.
* *
* @deprecated 9.3.0 Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array|object of WC_Product objects * @return array|object of WC_Product objects
*/ */
public function get_data() { public function get_data() {

View File

@ -16,12 +16,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Revenue;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Revenue\Query * API\Reports\Revenue\Query
*
* This query uses inconsistent names:
* - `report-revenue-stats` data store
* - `woocommerce_analytics_revenue_*` filters
* So, for backward compatibility, we cannot use GenericQuery.
*/ */
class Query extends ReportsQuery { class Query extends \WC_Object_Query {
/** /**
* Valid fields for Revenue report. * Valid fields for Revenue report.

View File

@ -13,7 +13,6 @@ use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery; use Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface; use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits; use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -60,37 +59,16 @@ class Controller extends GenericStatsController implements ExportableInterface {
} }
/** /**
* Get all reports. * Get data from RevenueQuery.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return WP_REST_Response|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query_args = $this->prepare_reports_query( $request ); $query = new RevenueQuery( $query_args );
$reports_revenue = new RevenueQuery( $query_args ); return $query->get_data();
try {
$report_data = $reports_revenue->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
);
} }
/** /**
@ -112,9 +90,9 @@ class Controller extends GenericStatsController implements ExportableInterface {
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param array $report Report data. * @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
@ -279,6 +257,7 @@ class Controller extends GenericStatsController implements ExportableInterface {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
unset( $params['fields'] );
return $params; return $params;
} }

View File

@ -0,0 +1,120 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\API\Reports;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
* Trait to contain *stats-specific methods for data stores.
*
* It does preliminary intervals & page calculations
* and prepares intervals & totals data structure by implementing the `get_noncached_data()` method.
* So, this time, you'll need to prepare `get_noncached_stats_data()` which will be called only if
* the requested page is within the date range.
*
* The trait also exposes the `initialize_queries()` method to initialize the interval and total queries.
*
* Example:
* <pre><code class="language-php">class MyStatsDataStore extends DataStore implements DataStoreInterface {
* // Use the trait.
* use StatsDataStoreTrait;
* // Provide all the necessary properties and methods for a regular DataStore.
* // ...
* /**
* * Return your results with the help of the interval & total methods and queries.
* * @return stdClass|WP_Error $data filled with your results.
* &ast;/
* public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
* $this->initialize_queries();
* // Do your magic ...
* // ... with a help of things like:
* $this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
* $this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
*
* $totals = $wpdb->get_results(
* $this->total_query->get_query_statement(),
* ARRAY_A
* );
*
* $intervals = $wpdb->get_results(
* $this->interval_query->get_query_statement(),
* ARRAY_A
* );
*
* $data->totals = (object) $this->cast_numbers( $totals[0] );
* $data->intervals = $intervals;
*
* 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 );
* }
*
* return $data;
* }
* }
* </code></pre>
*
* @see DataStore
*/
trait StatsDataStoreTrait {
/**
* Initialize query objects.
*/
protected function initialize_queries() {
$this->clear_all_clauses();
unset( $this->subquery );
$table_name = self::get_db_table_name();
$this->total_query = new SqlQuery( $this->context . '_total' );
$this->total_query->add_sql_clause( 'from', $table_name );
$this->interval_query = new SqlQuery( $this->context . '_interval' );
$this->interval_query->add_sql_clause( 'from', $table_name );
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
}
/**
* Returns the stats report data based on normalized parameters.
* Prepares the basic intervals and object structure
* Will be called by `get_data` if there is no data in cache.
* Will call `get_noncached_stats_data` to fetch the actual data.
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object, or error.
*/
public function get_noncached_data( $query_args ) {
$params = $this->get_limit_params( $query_args );
$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'] );
// Default, empty data object.
$data = (object) array(
'totals' => null,
'intervals' => array(),
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
// If the requested page is out off range, return the deault empty object.
if ( $query_args['page'] >= 1 && $query_args['page'] <= $total_pages ) {
// Fetch the actual data.
$data = $this->get_noncached_stats_data( $query_args, $params, $data, $expected_interval_count );
if ( ! is_wp_error( $data ) && is_array( $data->intervals ) ) {
$this->create_interval_subtotals( $data->intervals );
}
}
return $data;
}
}

View File

@ -276,9 +276,9 @@ class Controller extends GenericController implements ExportableInterface {
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param WC_Product $product Report data. * @param WC_Product $product Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */

View File

@ -47,9 +47,9 @@ class Controller extends \WC_REST_Reports_Controller {
} }
/** /**
* Prepare a report object for serialization. * Prepare a report data item for serialization.
* *
* @param WC_Product $report Report data. * @param WC_Product $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */

View File

@ -18,6 +18,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Get stock counts for the whole store. * Get stock counts for the whole store.
* *
* @override ReportsDataStore::get_data()
*
* @param array $query Not used for the stock stats data store, but needed for the interface. * @param array $query Not used for the stock stats data store, but needed for the interface.
* @return array Array of counts. * @return array Array of counts.
*/ */

View File

@ -10,12 +10,11 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Stock\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Stock\Stats\Query * API\Reports\Stock\Stats\Query
* This query takes no arguments, so we do not inherit from GenericQuery.
*/ */
class Query extends ReportsQuery { class Query extends \WC_Object_Query {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.

View File

@ -9,9 +9,10 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface; use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits; use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -34,6 +35,19 @@ class Controller extends GenericController implements ExportableInterface {
*/ */
protected $rest_base = 'reports/taxes'; protected $rest_base = 'reports/taxes';
/**
* Get data from `'taxes'` Query.
*
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'taxes' );
return $query->get_data();
}
/** /**
* Maps query arguments from the REST request. * Maps query arguments from the REST request.
* *
@ -55,41 +69,17 @@ class Controller extends GenericController implements ExportableInterface {
} }
/** /**
* Get all reports. * Prepare a report data item for serialization.
* *
* @param WP_REST_Request $request Request data. * @param mixed $report Report data item as returned from Data Store.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$taxes_query = new Query( $query_args );
$report_data = $taxes_query->get_data();
$data = array();
foreach ( $report_data->data as $tax_data ) {
$item = $this->prepare_item_for_response( (object) $tax_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$data,
(int) $report_data->total,
(int) $report_data->page_no,
(int) $report_data->pages
);
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report, $request ) {
$response = parent::prepare_item_for_response( $report, $request ); $response = parent::prepare_item_for_response( $report, $request );
// Map to `object` for backwards compatibility.
$report = (object) $report;
$response->add_links( $this->prepare_links( $report ) ); $response->add_links( $this->prepare_links( $report ) );
/** /**

View File

@ -21,6 +21,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_tax_lookup'; protected static $table_name = 'wc_order_tax_lookup';
@ -28,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'taxes'; protected $cache_key = 'taxes';
@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -53,12 +59,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'taxes'; protected $context = 'taxes';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
global $wpdb; global $wpdb;
@ -138,100 +148,97 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['orderby'] = 'tax_rate_id';
$defaults['taxes'] = array();
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $data = (object) array(
'page' => 1, 'data' => array(),
'order' => 'DESC', 'total' => 0,
'orderby' => 'tax_rate_id', 'pages' => 0,
'before' => TimeInterval::default_before(), 'page_no' => 0,
'after' => TimeInterval::default_after(),
'fields' => '*',
'taxes' => array(),
); );
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* $this->add_sql_query_params( $query_args );
* We need to get the cache key here because $params = $this->get_limit_params( $query_args );
* 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 ) { if ( isset( $query_args['taxes'] ) && is_array( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$this->initialize_queries(); $total_results = count( $query_args['taxes'] );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$data = (object) array( } else {
'data' => array(), $db_records_count = (int) $wpdb->get_var(
'total' => 0, // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- cache ok, DB call ok, unprepared SQL ok.
'pages' => 0, "SELECT COUNT(*) FROM ( {$this->subquery->get_query_statement()} ) AS tt"
'page_no' => 0,
); );
$this->add_sql_query_params( $query_args ); $total_results = $db_records_count;
$params = $this->get_limit_params( $query_args ); $total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( isset( $query_args['taxes'] ) && is_array( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) { if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
$total_results = count( $query_args['taxes'] );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
} else {
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$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->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$this->subquery->add_sql_clause( 'group_by', ", {$wpdb->prefix}woocommerce_order_items.order_item_name, {$wpdb->prefix}woocommerce_order_itemmeta.meta_value" );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$taxes_query = $this->subquery->get_query_statement();
$tax_data = $wpdb->get_results(
$taxes_query,
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $tax_data ) {
return $data; return $data;
} }
$tax_data = array_map( array( $this, 'cast_numbers' ), $tax_data );
$data = (object) array(
'data' => $tax_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
} }
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$this->subquery->add_sql_clause( 'group_by', ", {$wpdb->prefix}woocommerce_order_items.order_item_name, {$wpdb->prefix}woocommerce_order_itemmeta.meta_value" );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$taxes_query = $this->subquery->get_query_statement();
$tax_data = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$taxes_query,
ARRAY_A
);
if ( null === $tax_data ) {
return $data;
}
$tax_data = array_map( array( $this, 'cast_numbers' ), $tax_data );
$data = (object) array(
'data' => $tax_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
return $data; return $data;
} }
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* @return string * @return string
*/ */

View File

@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Taxes\Query * API\Reports\Taxes\Query
*
* @deprecated 9.3.0 Taxes\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Taxes report. * Valid fields for Taxes report.
* *
* @deprecated 9.3.0 Taxes\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Taxes\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController; use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -83,47 +84,30 @@ class Controller extends GenericStatsController {
} }
/** /**
* Get all reports. * Get data from `'taxes-stats'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return array|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query_args = $this->prepare_reports_query( $request ); $query = new GenericQuery( $query_args, 'taxes-stats' );
$taxes_query = new Query( $query_args ); return $query->get_data();
$report_data = $taxes_query->get_data();
$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( (object) $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. * Prepare a report data item for serialization.
* *
* @param stdClass $report Report data. * @param mixed $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
public function prepare_item_for_response( $report, $request ) { public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report ); $response = parent::prepare_item_for_response( $report, $request );
$response = parent::prepare_item_for_response( $data, $request );
// Map to `object` for backwards compatibility.
$report = (object) $report;
/** /**
* Filter a report returned from the API. * Filter a report returned from the API.
* *
@ -226,15 +210,6 @@ class Controller extends GenericStatsController {
), ),
'validate_callback' => 'rest_validate_request_arg', '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',
),
);
return $params; return $params;
} }

View File

@ -10,16 +10,19 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore; use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/** /**
* API\Reports\Taxes\Stats\DataStore. * API\Reports\Taxes\Stats\DataStore.
*/ */
class DataStore extends ReportsDataStore implements DataStoreInterface { class DataStore extends ReportsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_tax_lookup'; protected static $table_name = 'wc_order_tax_lookup';
@ -27,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'taxes_stats'; protected $cache_key = 'taxes_stats';
@ -34,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -47,12 +54,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'taxes_stats'; protected $context = 'taxes_stats';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -126,146 +137,116 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* @return stdClass|WP_Error Data. *
* @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['orderby'] = 'tax_rate_id';
$defaults['taxes'] = array();
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @see get_noncached_stats_data
* @param array $query_args Query parameters.
* @param array $params Query limit parameters.
* @param stdClass $data Reference to the data object to fill.
* @param int $expected_interval_count Number of expected intervals.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $selections = $this->selected_columns( $query_args );
'page' => 1, $order_stats_join = "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
'order' => 'DESC', $this->update_sql_query_params( $query_args );
'orderby' => 'tax_rate_id', $this->interval_query->add_sql_clause( 'join', $order_stats_join );
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(), $db_intervals = $wpdb->get_col(
'fields' => '*', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
'taxes' => array(), $this->interval_query->get_query_statement()
); );
$query_args = wp_parse_args( $query_args, $defaults ); $db_interval_count = count( $db_intervals );
$this->normalize_timezones( $query_args, $defaults );
/* $this->total_query->add_sql_clause( 'select', $selections );
* We need to get the cache key here because $this->total_query->add_sql_clause( 'join', $order_stats_join );
* parent::update_intervals_sql_params() modifies $query_args. $this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) { $totals = $wpdb->get_results(
$this->initialize_queries(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->total_query->get_query_statement(),
ARRAY_A
);
$data = (object) array( if ( null === $totals ) {
'totals' => (object) array(), return new \WP_Error( 'woocommerce_analytics_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
'intervals' => (object) array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$order_stats_join = "JOIN {$wpdb->prefix}wc_order_stats ON {$table_name}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
$this->update_sql_query_params( $query_args );
$this->interval_query->add_sql_clause( 'join', $order_stats_join );
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$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 $data;
}
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'join', $order_stats_join );
$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
// @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' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
$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' ) );
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_taxes_stats_result_failed', __( 'Sorry, fetching tax 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 );
} }
// phpcs:ignore Generic.Commenting.Todo.TaskFound
// @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' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
$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' ) );
$intervals = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- cache ok, DB call ok, unprepared SQL ok.
$this->interval_query->get_query_statement(),
ARRAY_A
);
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_taxes_stats_result_failed', __( 'Sorry, fetching tax data failed.', 'woocommerce' ) );
}
$totals = (object) $this->cast_numbers( $totals[0] );
$data->totals = $totals;
$data->intervals = $intervals;
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 );
return $data; return $data;
} }
/**
* 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' );
}
} }

View File

@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Taxes\Stats\Query * API\Reports\Taxes\Stats\Query
*
* @deprecated 9.3.0 Taxes\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Taxes report. * Valid fields for Taxes report.
* *
* @deprecated 9.3.0 Taxes\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/** /**
* Get tax stats data based on the current query vars. * Get tax stats data based on the current query vars.
* *
* @deprecated 9.3.0 Taxes\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,17 +9,24 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
defined( 'ABSPATH' ) || exit; 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\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits; use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/** /**
* REST API Reports products controller class. * REST API Reports products controller class.
* *
* @internal * @internal
* @extends ReportsController * @extends GenericController
*/ */
class Controller extends ReportsController implements ExportableInterface { class Controller extends GenericController implements ExportableInterface {
// The controller does not use this trait. It's here for API backward compatibility.
use OrderAwareControllerTrait;
/** /**
* Exportable traits. * Exportable traits.
*/ */
@ -43,13 +50,52 @@ class Controller extends ReportsController implements ExportableInterface {
); );
/** /**
* Get items. * Get data from `'variations'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* *
* @return array|WP_Error * @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'variations' );
return $query->get_data();
}
/**
* Prepare a report data item for serialization.
*
* @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
// Wrap the data in a response object.
$response = parent::prepare_item_for_response( $report, $request );
$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.
*
* @since 6.5.0
*
* @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 );
}
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array(); $args = array();
/** /**
* Experimental: Filter the list of parameters provided when querying data from the data store. * Experimental: Filter the list of parameters provided when querying data from the data store.
@ -57,6 +103,8 @@ class Controller extends ReportsController implements ExportableInterface {
* @ignore * @ignore
* *
* @param array $collection_params List of parameters. * @param array $collection_params List of parameters.
*
* @since 6.5.0
*/ */
$collection_params = apply_filters( $collection_params = apply_filters(
'experimental_woocommerce_analytics_variations_collection_params', 'experimental_woocommerce_analytics_variations_collection_params',
@ -72,54 +120,7 @@ class Controller extends ReportsController implements ExportableInterface {
} }
} }
} }
return $args;
$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 );
} }
/** /**
@ -244,38 +245,15 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array * @return array
*/ */
public function get_collection_params() { public function get_collection_params() {
$params = array(); $params = parent::get_collection_params();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); $params['orderby']['enum'] = array(
$params['page'] = array( 'date',
'description' => __( 'Current page of the collection.', 'woocommerce' ), 'net_revenue',
'type' => 'integer', 'orders_count',
'default' => 1, 'items_sold',
'sanitize_callback' => 'absint', 'sku',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
); );
$params['per_page'] = array( $params['match'] = 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' ), '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', 'type' => 'string',
'default' => 'all', 'default' => 'all',
@ -285,27 +263,7 @@ class Controller extends ReportsController implements ExportableInterface {
), ),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['order'] = array( $params['product_includes'] = 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' ), 'description' => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -315,7 +273,7 @@ class Controller extends ReportsController implements ExportableInterface {
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['product_excludes'] = array( $params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce' ), 'description' => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -325,7 +283,7 @@ class Controller extends ReportsController implements ExportableInterface {
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
); );
$params['variations'] = array( $params['variations'] = array(
'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce' ), 'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
@ -334,14 +292,14 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['extended_info'] = array( $params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each variation to the report.', 'woocommerce' ), 'description' => __( 'Add additional piece of info about each variation to the report.', 'woocommerce' ),
'type' => 'boolean', 'type' => 'boolean',
'default' => false, 'default' => false,
'sanitize_callback' => 'wc_string_to_bool', 'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['attribute_is'] = array( $params['attribute_is'] = array(
'description' => __( 'Limit result set to variations that include the specified attributes.', 'woocommerce' ), 'description' => __( 'Limit result set to variations that include the specified attributes.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -350,7 +308,7 @@ class Controller extends ReportsController implements ExportableInterface {
'default' => array(), 'default' => array(),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['attribute_is_not'] = array( $params['attribute_is_not'] = array(
'description' => __( 'Limit result set to variations that don\'t include the specified attributes.', 'woocommerce' ), 'description' => __( 'Limit result set to variations that don\'t include the specified attributes.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
@ -359,7 +317,7 @@ class Controller extends ReportsController implements ExportableInterface {
'default' => array(), 'default' => array(),
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['category_includes'] = array( $params['category_includes'] = array(
'description' => __( 'Limit result set to variations in the specified categories.', 'woocommerce' ), 'description' => __( 'Limit result set to variations in the specified categories.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
@ -368,7 +326,7 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['category_excludes'] = array( $params['category_excludes'] = array(
'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ), 'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',
@ -377,13 +335,7 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer', 'type' => 'integer',
), ),
); );
$params['force_cache_refresh'] = array( $params['products'] = 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',
);
$params['products'] = array(
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ), 'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list', 'sanitize_callback' => 'wp_parse_id_list',

View File

@ -9,7 +9,6 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore; use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/** /**
@ -20,6 +19,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Table used to get the data. * Table used to get the data.
* *
* @override ReportsDataStore::$table_name
*
* @var string * @var string
*/ */
protected static $table_name = 'wc_order_product_lookup'; protected static $table_name = 'wc_order_product_lookup';
@ -27,6 +28,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override ReportsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'variations'; protected $cache_key = 'variations';
@ -34,6 +37,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override ReportsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -70,12 +75,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override ReportsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'variations'; protected $context = 'variations';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -209,6 +218,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/** /**
* Maps ordering specified by the user to columns in the database/fields in the data. * Maps ordering specified by the user to columns in the database/fields in the data.
* *
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion. * @param string $order_by Sorting criterion.
* *
* @return string * @return string
@ -372,146 +383,139 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @param array $query_args Query parameters. * @override ReportsDataStore::get_default_query_vars()
* *
* @return stdClass|WP_Error Data. * @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['product_includes'] = array();
$defaults['variation_includes'] = array();
$defaults['extended_info'] = false;
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override ReportsDataStore::get_noncached_data()
*
* @see get_data
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_data( $query_args ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $data = (object) array(
'page' => 1, 'data' => array(),
'order' => 'DESC', 'total' => 0,
'orderby' => 'date', 'pages' => 0,
'before' => TimeInterval::default_before(), 'page_no' => 0,
'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 );
/* $selections = $this->selected_columns( $query_args );
* We need to get the cache key here because $included_variations =
* parent::update_intervals_sql_params() modifies $query_args. ( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) )
*/ ? $query_args['variation_includes']
$cache_key = $this->get_cache_key( $query_args ); : array();
$data = $this->get_cached_data( $cache_key ); $params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
if ( false === $data ) { if ( count( $included_variations ) > 0 ) {
$this->initialize_queries(); $total_results = count( $included_variations );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$data = (object) array( $this->subquery->clear_sql_clause( 'select' );
'data' => array(), $this->subquery->add_sql_clause( 'select', $selections );
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args ); if ( 'date' === $query_args['orderby'] ) {
$included_variations = $this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
( 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 */ $fields = $this->get_fields( $query_args );
$product_data = $wpdb->get_results( $join_selections = $this->format_join_selections( $fields, array( 'variation_id' ) );
$variations_query, $ids_table = $this->get_ids_table( $included_variations, 'variation_id' );
ARRAY_A
$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 */ /* phpcs:enable */
if ( null === $product_data ) { $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; return $data;
} }
$this->include_extended_info( $product_data, $query_args ); $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
if ( $query_args['extended_info'] ) { $variations_query = $this->subquery->get_query_statement();
$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 );
} }
/* 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'],
);
return $data; return $data;
} }

View File

@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Variations\Query * API\Reports\Variations\Query
*
* @deprecated 9.3.0 Variations\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Products report. * Valid fields for Products report.
* *
* @deprecated 9.3.0 Variations\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/** /**
* Get product data based on the current query vars. * Get product data based on the current query vars.
* *
* @deprecated 9.3.0 Variations\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -9,8 +9,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController; use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -46,12 +46,25 @@ class Controller extends GenericStatsController {
} }
/** /**
* Get all reports. * Get data from `'variations-stats'` Query.
* *
* @param WP_REST_Request $request Request data. * @override GenericController::get_datastore_data()
* @return array|WP_Error *
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/ */
public function get_items( $request ) { protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'variations-stats' );
return $query->get_data();
}
/**
* Maps query arguments from the REST request, to be fed to Query.
*
* @param \WP_REST_Request $request Full request object.
* @return array Simplified array of params.
*/
protected function prepare_reports_query( $request ) {
$query_args = array( $query_args = array(
'fields' => array( 'fields' => array(
'items_sold', 'items_sold',
@ -79,36 +92,13 @@ class Controller extends GenericStatsController {
} }
} }
$query = new Query( $query_args ); return $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. * Prepare a report data item for serialization.
* *
* @param array $report Report data. * @param array $report Report data item as returned from Data Store.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @return WP_REST_Response * @return WP_REST_Response
*/ */
@ -288,15 +278,6 @@ class Controller extends GenericStatsController {
), ),
'validate_callback' => 'rest_validate_request_arg', '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( $params['attribute_is'] = array(
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ), 'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',

View File

@ -10,16 +10,19 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/** /**
* API\Reports\Variations\Stats\DataStore. * API\Reports\Variations\Stats\DataStore.
*/ */
class DataStore extends VariationsDataStore implements DataStoreInterface { class DataStore extends VariationsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/** /**
* Mapping columns to data type to return correct response types. * Mapping columns to data type to return correct response types.
* *
* @override VariationsDataStore::$column_types
*
* @var array * @var array
*/ */
protected $column_types = array( protected $column_types = array(
@ -32,6 +35,8 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
/** /**
* Cache identifier. * Cache identifier.
* *
* @override VariationsDataStore::$cache_key
*
* @var string * @var string
*/ */
protected $cache_key = 'variations_stats'; protected $cache_key = 'variations_stats';
@ -39,12 +44,16 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
/** /**
* Data store context used to pass to filters. * Data store context used to pass to filters.
* *
* @override VariationsDataStore::$context
*
* @var string * @var string
*/ */
protected $context = 'variations_stats'; protected $context = 'variations_stats';
/** /**
* Assign report columns once full table name has been assigned. * Assign report columns once full table name has been assigned.
*
* @override VariationsDataStore::assign_report_columns()
*/ */
protected function assign_report_columns() { protected function assign_report_columns() {
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
@ -133,144 +142,131 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
} }
/** /**
* Returns the report data based on parameters supplied by the user. * Get the default query arguments to be used by get_data().
* These defaults are only partially applied when used via REST API, as that has its own defaults.
* *
* @since 3.5.0 * @override VariationsDataStore::get_default_query_vars()
* @param array $query_args Query parameters. *
* @return stdClass|WP_Error Data. * @return array Query parameters.
*/ */
public function get_data( $query_args ) { public function get_default_query_vars() {
$defaults = parent::get_default_query_vars();
$defaults['category_includes'] = array();
$defaults['interval'] = 'week';
unset( $defaults['extended_info'] );
return $defaults;
}
/**
* Returns the report data based on normalized parameters.
* Will be called by `get_data` if there is no data in cache.
*
* @override VariationsDataStore::get_noncached_stats_data()
*
* @see get_data
* @see get_noncached_stats_data
* @param array $query_args Query parameters.
* @param array $params Query limit parameters.
* @param stdClass $data Reference to the data object to fill.
* @param int $expected_interval_count Number of expected intervals.
* @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error.
*/
public function get_noncached_stats_data( $query_args, $params, &$data, $expected_interval_count ) {
global $wpdb; global $wpdb;
$table_name = self::get_db_table_name(); $table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults. $this->initialize_queries();
$defaults = array(
'per_page' => get_option( 'posts_per_page' ), $selections = $this->selected_columns( $query_args );
'page' => 1,
'order' => 'DESC', $this->update_sql_query_params( $query_args );
'orderby' => 'date', $this->get_limit_sql_params( $query_args );
'before' => TimeInterval::default_before(), $this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
'after' => TimeInterval::default_after(),
'fields' => '*', /* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
'category_includes' => array(), $db_intervals = $wpdb->get_col(
'interval' => 'week', $this->interval_query->get_query_statement()
'product_includes' => array(),
'variation_includes' => array(),
); );
$query_args = wp_parse_args( $query_args, $defaults ); /* phpcs:enable */
$this->normalize_timezones( $query_args, $defaults );
/* $db_interval_count = count( $db_intervals );
* 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 ) { $intervals = array();
$this->initialize_queries(); $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' ) );
$selections = $this->selected_columns( $query_args ); /* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$params = $this->get_limit_params( $query_args ); $totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
$this->update_sql_query_params( $query_args ); // phpcs:ignore Generic.Commenting.Todo.TaskFound
$this->get_limit_sql_params( $query_args ); // @todo remove these assignements when refactoring segmenter classes to use query objects.
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) ); $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 );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */ if ( null === $totals ) {
$db_intervals = $wpdb->get_col( return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
$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 );
} }
$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->totals = $totals;
$data->intervals = $intervals;
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 );
return $data; return $data;
} }
/** /**
* Normalizes order_by clause to match to SQL query. * Normalizes order_by clause to match to SQL query.
* *
* @override VariationsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user. * @param string $order_by Order by option requeste by user.
* @return string * @return string
*/ */
@ -281,18 +277,4 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
return $order_by; 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' );
}
} }

View File

@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/** /**
* API\Reports\Variations\Stats\Query * API\Reports\Variations\Stats\Query
*
* @deprecated 9.3.0 Variations\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/ */
class Query extends ReportsQuery { class Query extends ReportsQuery {
/** /**
* Valid fields for Products report. * Valid fields for Products report.
* *
* @deprecated 9.3.0 Variations\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
protected function get_default_query_vars() { protected function get_default_query_vars() {
@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/** /**
* Get variations data based on the current query vars. * Get variations data based on the current query vars.
* *
* @deprecated 9.3.0 Variations\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array * @return array
*/ */
public function get_data() { public function get_data() {

View File

@ -6,7 +6,7 @@
*/ */
use Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore as CouponsStatsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore as CouponsStatsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Query as CouponsStatsQuery; use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
/** /**
* Class WC_Admin_Tests_Reports_Coupons_Stats * Class WC_Admin_Tests_Reports_Coupons_Stats
@ -100,8 +100,8 @@ class WC_Admin_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new CouponsStatsQuery( $args ); $query = new GenericQuery( $args, 'coupons-stats' );
$this->assertEquals( $expected_data, $query->get_data() ); $this->assertEquals( $expected_data, $query->get_data() );
} }
@ -143,8 +143,8 @@ class WC_Admin_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case {
'interval' => 'day', 'interval' => 'day',
); );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new CouponsStatsQuery( $args ); $query = new GenericQuery( $args, 'coupons-stats' );
$start_datetime = new DateTime( $start_time ); $start_datetime = new DateTime( $start_time );
$end_datetime = new DateTime( $end_time ); $end_datetime = new DateTime( $end_time );
$expected_data = (object) array( $expected_data = (object) array(

View File

@ -5,9 +5,9 @@
* @package WooCommerce\Admin\Tests\Coupons * @package WooCommerce\Admin\Tests\Coupons
*/ */
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\ReportCSVExporter; use Automattic\WooCommerce\Admin\ReportCSVExporter;
use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Coupons\Query as CouponsQuery;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/** /**
@ -96,8 +96,8 @@ class WC_Admin_Tests_Reports_Coupons extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new CouponsQuery( $args ); $query = new GenericQuery( $args, 'coupons' );
$this->assertEquals( $expected_data, $query->get_data() ); $this->assertEquals( $expected_data, $query->get_data() );
// Test order by orders_count DESC. // Test order by orders_count DESC.

View File

@ -6,8 +6,6 @@
*/ */
use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore; use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Orders\Query as OrdersQuery;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
/** /**
* Class WC_Admin_Tests_Reports_Orders * Class WC_Admin_Tests_Reports_Orders

View File

@ -6,8 +6,8 @@
* @todo Finish up unit testing to verify bug-free product reports. * @todo Finish up unit testing to verify bug-free product reports.
*/ */
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Products\DataStore as ProductsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Products\Query as ProductsQuery;
use Automattic\WooCommerce\Admin\ReportCSVExporter; use Automattic\WooCommerce\Admin\ReportCSVExporter;
/** /**
@ -70,8 +70,8 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new ProductsQuery( $args ); $query = new GenericQuery( $args, 'products' );
$this->assertEquals( $expected_data, $query->get_data() ); $this->assertEquals( $expected_data, $query->get_data() );
} }
@ -186,8 +186,8 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new ProductsQuery( $args ); $query = new GenericQuery( $args, 'products' );
$this->assertEquals( $expected_data, $query->get_data() ); $this->assertEquals( $expected_data, $query->get_data() );
} }
@ -411,8 +411,8 @@ class WC_Admin_Tests_Reports_Products extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new ProductsQuery( $args ); $query = new GenericQuery( $args, 'products' );
$this->assertEquals( $expected_data, $query->get_data() ); $this->assertEquals( $expected_data, $query->get_data() );
} }

View File

@ -6,8 +6,8 @@
* @todo Finish up unit testing to verify bug-free order reports. * @todo Finish up unit testing to verify bug-free order reports.
*/ */
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore; use Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Variations\Query as VariationsQuery;
/** /**
* Reports order stats tests class. * Reports order stats tests class.
@ -71,8 +71,8 @@ class WC_Admin_Tests_Reports_Variations extends WC_Unit_Test_Case {
); );
$this->assertEquals( $expected_data, $data ); $this->assertEquals( $expected_data, $data );
// Test retrieving the stats through the query class. // Test retrieving the stats through the generic query class.
$query = new VariationsQuery( $args ); $query = new GenericQuery( $args, 'variations' );
$this->assertEquals( $expected_data, $query->get_data() ); $this->assertEquals( $expected_data, $query->get_data() );
} }