plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -0,0 +1,57 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
/**
* AI Endpoint base controller
*
* @internal
*/
abstract class AIEndpoint {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'ai';
/**
* Endpoint.
*
* @var string
*/
protected $endpoint;
/**
* Register routes.
*
* @param array $args Optional. Either an array of options for the endpoint,
* or an array of arrays for multiple methods. Default empty array.
*/
public function register( $args ) {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/' . $this->endpoint,
$args
);
}
/**
* Return schema properties.
*
* @return array
*/
public function get_schema() {
return array();
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Store Title controller
*
* @internal
*/
class BusinessDescription extends AIEndpoint {
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'business-description';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'update_business_description' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'business_description' => array(
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_schema' ),
)
);
}
/**
* Update the business description.
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object.
*/
public function update_business_description( $request ) {
$business_description = $request->get_param( 'business_description' );
if ( ! $business_description ) {
return new WP_Error(
'invalid_business_description',
__( 'Invalid business description.', 'woocommerce' )
);
}
update_option( 'last_business_description_with_ai_content_generated', $business_description );
return rest_ensure_response(
array(
'ai_content_generated' => true,
)
);
}
/**
* Get the Business Description response.
*
* @return array
*/
public function get_schema() {
return array(
'ai_content_generated' => true,
);
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Images\Pexels;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Images controller
*
* @internal
*/
class Images extends AIEndpoint {
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'images';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'generate_images' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'business_description' => array(
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
),
),
),
)
);
}
/**
* Generate Images from Pexels
*
* @param WP_REST_Request $request Request object.
*
* @return WP_Error|WP_REST_Response
*/
public function generate_images( WP_REST_Request $request ) {
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
if ( empty( $business_description ) ) {
$business_description = get_option( 'woo_ai_describe_store_description' );
}
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
if ( $last_business_description === $business_description ) {
return rest_ensure_response(
array(
'ai_content_generated' => true,
'images' => array(),
),
);
}
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
if ( is_wp_error( $images ) ) {
$images = array(
'images' => array(),
'search_term' => '',
);
}
return rest_ensure_response(
array(
'ai_content_generated' => true,
'images' => $images,
)
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use WP_Error;
/**
* Middleware class.
*
* @internal
*/
class Middleware {
/**
* Ensure that the user is allowed to make this request.
*
* @return boolean|WP_Error
* @throws RouteException If the user is not allowed to make this request.
*/
public static function is_authorized() {
try {
if ( ! current_user_can( 'manage_options' ) ) {
throw new RouteException( 'woocommerce_rest_invalid_user', __( 'You are not allowed to make this request. Please make sure you are logged in.', 'woocommerce' ), 403 );
}
} catch ( RouteException $error ) {
return new WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
if ( ! $allow_ai_connection ) {
try {
throw new RouteException( 'ai_connection_not_allowed', __( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' ), 403 );
} catch ( RouteException $error ) {
return new WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
}
return true;
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper;
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Patterns controller
*
* @internal
*/
class Patterns extends AIEndpoint {
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'patterns';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'update_patterns' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'business_description' => array(
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
),
'images' => array(
'description' => __( 'The images for a given store.', 'woocommerce' ),
'type' => 'object',
),
),
),
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_patterns' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
),
)
);
}
/**
* Update patterns with the content and images powered by AI.
*
* @param WP_REST_Request $request Request object.
*
* @return WP_Error|WP_REST_Response
*/
public function update_patterns( WP_REST_Request $request ) {
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
$images = $request['images'];
try {
( new UpdatePatterns() )->generate_content( $ai_connection, $token, $images, $business_description );
return rest_ensure_response( array( 'ai_content_generated' => true ) );
} catch ( \Exception $e ) {
return rest_ensure_response( array( 'ai_content_generated' => false ) );
}
}
/**
* Remove patterns generated by AI.
*
* @return WP_Error|WP_REST_Response
*/
public function delete_patterns() {
PatternsHelper::delete_patterns_ai_data_post();
return rest_ensure_response( array( 'removed' => true ) );
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Product controller
*
* @internal
*/
class Product extends AIEndpoint {
/**
* The endpoint response option name.
*
* @var string
*/
const AI_CONTENT_GENERATED = 'ai_content_generated';
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'product';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'update_product' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'products_information' => array(
'description' => __( 'Data generated by AI for updating dummy products.', 'woocommerce' ),
'type' => 'object',
),
'last_product' => array(
'description' => __( 'Whether the product being updated is the last one in the loop', 'woocommerce' ),
'type' => 'boolean',
),
),
),
)
);
}
/**
* Update product with the content and images powered by AI.
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response
*/
public function update_product( WP_REST_Request $request ) {
$product_information = $request['products_information'] ?? array();
if ( empty( $product_information ) ) {
return rest_ensure_response(
array(
self::AI_CONTENT_GENERATED => true,
)
);
}
try {
$product_updater = new UpdateProducts();
$product_updater->update_product_content( $product_information );
} catch ( \Exception $e ) {
return rest_ensure_response( array( 'ai_content_generated' => false ) );
}
$last_product_to_update = $request['last_product'] ?? false;
if ( $last_product_to_update ) {
flush_rewrite_rules();
}
return rest_ensure_response(
array(
self::AI_CONTENT_GENERATED => true,
)
);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper;
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Store Info controller
*
* @internal
*/
class StoreInfo extends AIEndpoint {
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'store-info';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_response' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
),
'schema' => array( $this, 'get_schema' ),
)
);
}
/**
* Update the store title powered by AI.
*
* @return WP_Error|WP_REST_Response
*/
public function get_response() {
$product_updater = new UpdateProducts();
$patterns = PatternsHelper::get_patterns_ai_data_post();
$products = $product_updater->fetch_product_ids( 'dummy' );
if ( empty( $products ) && ! isset( $patterns ) ) {
return rest_ensure_response(
array(
'is_ai_generated' => false,
)
);
}
return rest_ensure_response(
array(
'is_ai_generated' => true,
)
);
}
/**
* Get the Business Description response.
*
* @return array
*/
public function get_schema() {
return array(
'ai_content_generated' => true,
);
}
}

View File

@@ -0,0 +1,155 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\API\AI;
use Automattic\WooCommerce\Blocks\AI\Connection;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
defined( 'ABSPATH' ) || exit;
/**
* Store Title controller
*
* @internal
*/
class StoreTitle extends AIEndpoint {
/**
* The store title option name.
*
* @var string
*/
const STORE_TITLE_OPTION_NAME = 'blogname';
/**
* The AI generated store title option name.
*
* @var string
*/
const AI_STORE_TITLE_OPTION_NAME = 'ai_generated_site_title';
/**
* The default store title.
*
* @var string
*/
const DEFAULT_TITLE = 'Site Title';
/**
* Endpoint.
*
* @var string
*/
protected $endpoint = 'store-title';
/**
* Register routes.
*/
public function register_routes() {
$this->register(
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'update_store_title' ),
'permission_callback' => array( Middleware::class, 'is_authorized' ),
'args' => array(
'business_description' => array(
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_schema' ),
)
);
}
/**
* Update the store title powered by AI.
*
* @param WP_REST_Request $request Request object.
*
* @return WP_Error|WP_REST_Response
*/
public function update_store_title( $request ) {
$business_description = $request->get_param( 'business_description' );
if ( ! $business_description ) {
return new WP_Error(
'invalid_business_description',
__( 'Invalid business description.', 'woocommerce' )
);
}
$store_title = html_entity_decode( get_option( self::STORE_TITLE_OPTION_NAME, '' ) );
$previous_ai_generated_title = html_entity_decode( get_option( self::AI_STORE_TITLE_OPTION_NAME, '' ) );
if ( strtolower( trim( self::DEFAULT_TITLE ) ) === strtolower( trim( $store_title ) ) || ( ! empty( $store_title ) && $previous_ai_generated_title !== $store_title ) ) {
return rest_ensure_response( array( 'ai_content_generated' => false ) );
}
$ai_generated_title = $this->generate_ai_title( $business_description );
if ( is_wp_error( $ai_generated_title ) ) {
return $ai_generated_title;
}
update_option( self::AI_STORE_TITLE_OPTION_NAME, $ai_generated_title );
update_option( self::STORE_TITLE_OPTION_NAME, $ai_generated_title );
return rest_ensure_response(
array(
'ai_content_generated' => true,
)
);
}
/**
* Generate the store title powered by AI.
*
* @param string $business_description The business description for a given store.
*
* @return string|WP_Error|WP_REST_Response The store title generated by AI.
*/
private function generate_ai_title( $business_description ) {
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
$prompt = "Generate a store title for a store that has the following: '$business_description'. The length of the title should be 1 and 3 words. The result should include only the store title without any other explanation, number or punctuation marks";
$ai_response = $ai_connection->fetch_ai_response( $token, $prompt );
if ( is_wp_error( $ai_response ) ) {
return $ai_response;
}
if ( ! isset( $ai_response['completion'] ) ) {
return '';
}
return $ai_response['completion'];
}
/**
* Get the Business Description response.
*
* @return array
*/
public function get_schema() {
return array(
'ai_content_generated' => true,
);
}
}

View File

@@ -86,6 +86,12 @@ class Init {
'Automattic\WooCommerce\Admin\API\NavigationFavorites',
'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
'Automattic\WooCommerce\Admin\API\ShippingPartnerSuggestions',
'Automattic\WooCommerce\Admin\API\AI\StoreTitle',
'Automattic\WooCommerce\Admin\API\AI\BusinessDescription',
'Automattic\WooCommerce\Admin\API\AI\StoreInfo',
'Automattic\WooCommerce\Admin\API\AI\Images',
'Automattic\WooCommerce\Admin\API\AI\Patterns',
'Automattic\WooCommerce\Admin\API\AI\Product',
);
}

View File

@@ -133,7 +133,7 @@ class LaunchYourStore {
$private_link = 'no';
$share_key = wp_generate_password( 32, false );
add_option( 'woocommerce_coming_soon', $coming_soon );
update_option( 'woocommerce_coming_soon', $coming_soon );
add_option( 'woocommerce_store_pages_only', $store_pages_only );
add_option( 'woocommerce_private_link', $private_link );
add_option( 'woocommerce_share_key', $share_key );

View File

@@ -9,16 +9,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
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\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/**
* REST API Reports categories controller class.
*
* @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.
@@ -27,6 +31,19 @@ class Controller extends ReportsController implements ExportableInterface {
*/
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.
*
@@ -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.
* @return array|WP_Error
*/
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
* @param mixed $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 ) {
$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 = parent::prepare_item_for_response( $report, $request );
$response->add_links( $this->prepare_links( $report ) );
/**
@@ -119,7 +95,7 @@ class Controller extends ReportsController implements ExportableInterface {
/**
* 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
*/
protected function prepare_links( $object ) {
@@ -193,59 +169,17 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
$params = parent::get_collection_params();
$params['orderby']['default'] = 'category_id';
$params['orderby']['enum'] = array(
'category_id',
'items_sold',
'net_revenue',
'orders_count',
'products_count',
'category',
);
$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' => 'category_id',
'enum' => array(
'category_id',
'items_sold',
'net_revenue',
'orders_count',
'products_count',
'category',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
@@ -259,7 +193,7 @@ class Controller extends ReportsController implements ExportableInterface {
),
'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' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
@@ -269,7 +203,7 @@ class Controller extends ReportsController implements ExportableInterface {
'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' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
@@ -279,7 +213,7 @@ class Controller extends ReportsController implements ExportableInterface {
'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' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@@ -288,19 +222,13 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer',
),
);
$params['extended_info'] = array(
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each category to the report.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'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;
}

View File

@@ -9,7 +9,6 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
@@ -20,6 +19,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_product_lookup';
@@ -27,6 +28,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'categories';
@@ -48,6 +51,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -61,12 +66,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'categories';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
*
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion.
* @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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @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;
$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.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'extended_info' => false,
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$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 );
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$included_categories = $this->get_included_categories_array( $query_args );
$this->add_sql_query_params( $query_args );
if ( false === $data ) {
$this->initialize_queries();
if ( count( $included_categories ) > 0 ) {
$fields = $this->get_fields( $query_args );
$ids_table = $this->get_ids_table( $included_categories, 'category_id' );
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
$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"
);
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$included_categories = $this->get_included_categories_array( $query_args );
$this->add_sql_query_params( $query_args );
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_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'],
);
return $data;
}
/**
* Initialize query objects.
*
* @override ReportsDataStore::initialize_queries()
*/
protected function initialize_queries() {
global $wpdb;

View File

@@ -21,7 +21,9 @@ defined( 'ABSPATH' ) || exit;
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 {
@@ -30,6 +32,8 @@ class Query extends ReportsQuery {
/**
* Valid fields for Categories report.
*
* @deprecated 9.3.0 Categories\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array
*/
protected function get_default_query_vars() {
@@ -39,6 +43,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -1,8 +1,6 @@
<?php
/**
* REST API Reports controller extended by WC Admin plugin.
*
* Handles requests to the reports endpoint.
* REST API Reports controller extended to handle requests to the reports endpoint.
*/
namespace Automattic\WooCommerce\Admin\API\Reports;
@@ -10,15 +8,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports;
defined( 'ABSPATH' ) || exit;
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
* @extends GenericController
*/
class Controller extends GenericController {
use OrderAwareControllerTrait;
/**
* Get all reports.
*
@@ -135,71 +138,6 @@ class Controller extends GenericController {
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.
*
@@ -214,12 +152,8 @@ class Controller extends GenericController {
'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.
$response = rest_ensure_response( $data );
$response = parent::prepare_item_for_response( $data, $request );
$response->add_links(
array(
'self' => array(
@@ -249,6 +183,8 @@ class Controller extends GenericController {
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @override WP_REST_Controller::get_item_schema()
*
* @return array
*/
public function get_item_schema() {
@@ -291,42 +227,4 @@ class Controller extends GenericController {
'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\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use WP_REST_Request;
use WP_REST_Response;
@@ -29,6 +30,19 @@ class Controller extends GenericController implements ExportableInterface {
*/
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.
*
@@ -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.
* @return array|WP_Error
*/
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
* @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 ) {
$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.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_coupon_lookup';
@@ -28,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'coupons';
@@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -46,12 +52,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'coupons';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
*
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion.
* @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.
*
@@ -363,6 +262,115 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
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.
*

View File

@@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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 {
/**
* Valid fields for Products report.
*
* @deprecated 9.3.0 Coupons\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array
*/
protected function get_default_query_vars() {
@@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -10,7 +10,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats;
defined( 'ABSPATH' ) || exit;
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_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.
* @return array|WP_Error
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$coupons_query = new Query( $query_args );
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
);
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'coupons-stats' );
return $query->get_data();
}
/**
* 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.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$response = parent::prepare_item_for_response( $data, $request );
$response = parent::prepare_item_for_response( $report, $request );
// Map to `object` for backwards compatibility.
$report = (object) $report;
/**
* Filter a report returned from the API.
*
@@ -189,15 +168,6 @@ class Controller extends GenericStatsController {
),
'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;
}

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\DataStoreInterface;
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.
*/
class DataStore extends CouponsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/**
* Mapping columns to data type to return correct response types.
*
* @override CouponsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -33,6 +37,8 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
/**
* SQL columns to select in the db query.
*
* @override CouponsDataStore::$report_columns
*
* @var array
*/
protected $report_columns;
@@ -40,6 +46,8 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override CouponsDataStore::$context
*
* @var string
*/
protected $context = 'coupons_stats';
@@ -47,12 +55,16 @@ class DataStore extends CouponsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override CouponsDataStore::get_default_query_vars()
*
* @var string
*/
protected $cache_key = 'coupons_stats';
/**
* Assign report columns once full table name has been assigned.
*
* @override CouponsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
* @override CouponsDataStore::get_default_query_vars()
*
* @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;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'interval' => 'week',
'coupons' => array(),
$this->initialize_queries();
$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(
// 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 );
/*
* 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 );
$db_interval_count = count( $db_intervals );
if ( false === $data ) {
$this->initialize_queries();
$this->total_query->add_sql_clause( 'select', $selections );
$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(
'data' => array(),
'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 );
if ( null === $totals ) {
return $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;
}
/**
* 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
*
* @deprecated 9.3.0 Coupons\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/
class Query extends ReportsQuery {
/**
* 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
*/
protected function get_default_query_vars() {
@@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -33,6 +33,19 @@ class Controller extends GenericController implements ExportableInterface {
*/
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.
*
@@ -84,34 +97,6 @@ class Controller extends GenericController implements ExportableInterface {
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.
*
@@ -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 WP_REST_Request $request Request object.
* @return WP_REST_Response
* @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 ) {
$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.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_customer_lookup';
@@ -29,6 +31,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'customers';
@@ -36,6 +40,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -49,12 +55,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'customers';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
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.
*
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion.
* @return string
*/
@@ -182,6 +194,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* 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 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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @return array Query parameters.
*/
public function get_data( $query_args ) {
global $wpdb;
public function get_default_query_vars() {
$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();
$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;
return $defaults;
}
/**
@@ -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.
*

View File

@@ -16,14 +16,23 @@
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
*/
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.
@@ -39,17 +48,4 @@ class Query extends ReportsQuery {
'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;
use Automattic\WooCommerce\Admin\API\Reports\Customers\Query;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
@@ -83,7 +85,7 @@ class Controller extends \WC_REST_Reports_Controller {
*/
public function get_items( $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();
$out_data = array(
'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 WP_REST_Request $request Request object.
* @return WP_REST_Response
* @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 ) {
$data = $report;

View File

@@ -8,6 +8,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Customers\Stats;
defined( 'ABSPATH' ) || exit;
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;
/**
@@ -17,6 +18,8 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override CustomersDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -29,6 +32,8 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override CustomersDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'customers_stats';
@@ -36,12 +41,16 @@ class DataStore extends CustomersDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override CustomersDataStore::$context
*
* @var string
*/
protected $context = 'customers_stats';
/**
* Assign report columns once full table name has been assigned.
*
* @override CustomersDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
* @return stdClass|WP_Error Data.
* @override CustomersDataStore::get_default_query_vars()
*
* @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;
$this->initialize_queries();
$customers_table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_registered',
'fields' => '*',
$data = (object) array(
'customers_count' => 0,
'avg_orders_count' => 0,
'avg_total_spend' => 0.0,
'avg_avg_order_value' => 0.0,
);
$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 );
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
// 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'
);
if ( false === $data ) {
$this->initialize_queries();
$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" );
$data = (object) array(
'customers_count' => 0,
'avg_orders_count' => 0,
'avg_total_spend' => 0.0,
'avg_avg_order_value' => 0.0,
);
$report_data = $wpdb->get_results(
$this->get_query_statement(), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
);
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
// 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 );
if ( null === $report_data ) {
return $data;
}
$data = (object) $this->cast_numbers( $report_data[0] );
return $data;
}
}

View File

@@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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 {
/**
* 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
*/
protected function get_default_query_vars() {
@@ -43,6 +47,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -9,12 +9,59 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
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.
@@ -90,6 +137,8 @@ class DataStore extends SqlQuery {
/**
* Data store context used to pass to filters.
*
* @override SqlQuery
*
* @var string
*/
protected $context = 'reports';
@@ -138,6 +187,8 @@ class DataStore extends SqlQuery {
/**
* Class constructor.
*
* @override SqlQuery::__construct()
*/
public function __construct() {
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.
*/
@@ -168,6 +267,19 @@ class DataStore extends SqlQuery {
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.
*/

View File

@@ -9,16 +9,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
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\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\OrderAwareControllerTrait;
/**
* REST API Reports downloads controller class.
*
* @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.
@@ -28,67 +32,40 @@ class Controller extends ReportsController implements ExportableInterface {
protected $rest_base = 'reports/downloads';
/**
* Get items.
* Get data from `'downloads'` Query.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
public function get_items( $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 ];
}
}
$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
);
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'downloads' );
return $query->get_data();
}
/**
* 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.
* @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 = parent::prepare_item_for_response( $report, $request );
$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.
// 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 );
// Make sure the product hasn't been deleted.
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 );
$response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
$response->data['file_path'] = $file_path;
@@ -97,9 +74,9 @@ class Controller extends ReportsController implements ExportableInterface {
$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['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.
@@ -130,6 +107,22 @@ class Controller extends ReportsController implements ExportableInterface {
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.
*
@@ -225,53 +218,10 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['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 = parent::get_collection_params();
$params['orderby']['enum'] = array(
'date',
'product',
);
$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' ),
@@ -355,12 +305,6 @@ class Controller extends ReportsController implements ExportableInterface {
'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;
}

View File

@@ -20,6 +20,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_download_log';
@@ -27,6 +29,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'downloads';
@@ -34,6 +38,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -51,12 +57,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'downloads';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
*
* @override ReportsDataStore::add_time_period_sql_params()
*
* @param array $query_args Parameters supplied by the user.
* @param string $table_name Name of the db table relevant for the date constraint.
* @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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @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;
$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.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'timestamp',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$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 );
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
if ( false === $data ) {
$this->initialize_queries();
// 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
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'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 );
$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'],
);
return $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.
* @return string
*/

View File

@@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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 {
/**
* Valid fields for downloads report.
*
* @deprecated 9.3.0 Downloads\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array
*/
protected function get_default_query_vars() {
@@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use WP_REST_Request;
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.
* @return array|WP_Error
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$downloads_query = new Query( $query_args );
$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
);
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'downloads-stats' );
return $query->get_data();
}
/**
* 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.
* @return WP_REST_Response
*/
@@ -110,7 +94,6 @@ class Controller extends GenericStatsController {
return apply_filters( 'woocommerce_rest_prepare_report_downloads_stats', $response, $report, $request );
}
/**
* Get the Report's item properties schema.
* 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.
* It does not have the segments as in GenericStatsController.
@@ -298,15 +282,6 @@ class Controller extends GenericStatsController {
'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;
}

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\DataStoreInterface;
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.
*/
class DataStore extends DownloadsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/**
* Mapping columns to data type to return correct response types.
*
* @override DownloadsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -29,6 +32,8 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override DownloadsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'downloads_stats';
@@ -36,12 +41,16 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override DownloadsDataStore::$context
*
* @var string
*/
protected $context = 'downloads_stats';
/**
* Assign report columns once full table name has been assigned.
*
* @override DownloadsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
* @return stdClass|WP_Error Data.
* @override DownloadsDataStore::default_query_args()
*
* @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;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'fields' => '*',
'interval' => 'week',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
$where_time = $this->add_time_period_sql_params( $query_args, $table_name );
$this->add_intervals_sql_params( $query_args, $table_name );
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
$this->interval_query->str_replace_clause( 'select', 'date_created', 'timestamp' );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$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 );
/*
* 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 );
$db_records_count = count( $db_intervals );
if ( false === $data ) {
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$this->add_sql_query_params( $query_args );
$where_time = $this->add_time_period_sql_params( $query_args, $table_name );
$this->add_intervals_sql_params( $query_args, $table_name );
$this->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$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(
// 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->str_replace_clause( 'select', 'date_created', 'timestamp' );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$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(timestamp) AS datetime_anchor' );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
); // phpcs:ignore cache ok, DB call ok, unprepared SQL ok.
$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
);
$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 );
$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();
}
$totals = (object) $this->cast_numbers( $totals[0] );
$this->update_intervals_sql_params( $query_args, $db_records_count, $expected_interval_count, $table_name );
$this->interval_query->str_replace_clause( 'where_time', 'date_created', 'timestamp' );
$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' ) );
}
$data->totals = $totals;
$data->intervals = $intervals;
$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(timestamp) 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_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 );
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 );
}
return $data;
@@ -163,6 +161,8 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
/**
* Normalizes order_by clause to match to SQL query.
*
* @override DownloadsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user.
* @return string
*/
@@ -173,18 +173,4 @@ class DataStore extends DownloadsDataStore implements DataStoreInterface {
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
*
* @deprecated 9.3.0 Downloads\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/
class Query extends ReportsQuery {
/**
* 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
*/
protected function get_default_query_vars() {
@@ -26,6 +30,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
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;
/**
* WC REST API Reports controller extended
* to be shared as a generic base for all Analytics controllers.
* {@see WC_REST_Reports_Controller WC REST API Reports Controller} extended to be shared as a generic base for all Analytics reports 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
*/
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.
*
* @param WP_REST_Request $request Request data.
* @param WP_REST_Response|array $response Response data.
* @param int $total Total results.
* @param int $page Current page.
* @param int $max_pages Total amount of pages.
* @return WP_REST_Response
* @param \WP_REST_Request $request Request data.
* @param \WP_REST_Response|array $response Response data.
* @param int $total Total results.
* @param int $page Current page.
* @param int $max_pages Total amount of pages.
* @return \WP_REST_Response
*/
public function add_pagination_headers( $request, $response, int $total, int $page, int $max_pages ) {
$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
*/
@@ -124,15 +171,62 @@ abstract class GenericController extends \WC_REST_Reports_Controller {
return $params;
}
/**
* Prepare a report object for serialization.
* Get the report data.
*
* @param array $report Report data.
* @param WP_REST_Request $request Request object.
* 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.
*
* @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
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
public function prepare_item_for_response( $report_item, $request ) {
$data = $report_item;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$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.
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;
/**
* 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
*/
abstract class GenericStatsController extends GenericController {
/**
* Get the query params for collections.
* Adds intervals to the generic list.
* Get the query params definition for collections.
* Adds `fields` & `intervals` to the generic list.
*
* @override GenericController::get_collection_params()
*
* @return array
*/
public function 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(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'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`.
*
* @return array
@@ -50,7 +98,7 @@ abstract class GenericStatsController extends GenericController {
/**
* 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.
*
* @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;
use Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
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.
*
* @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.
@@ -27,6 +30,19 @@ class Controller extends ReportsController implements ExportableInterface {
*/
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.
*
@@ -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.
* @return array|WP_Error
*/
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
* @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 ) {
$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 );
$report['order_number'] = $this->get_order_number( $report['order_id'] );
$report['total_formatted'] = $this->get_total_formatted( $report['order_id'] );
// 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 ) );
/**
@@ -248,54 +231,12 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 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 = parent::get_collection_params();
$params['per_page']['minimum'] = 0;
$params['orderby']['enum'] = array(
'date',
'num_items_sold',
'net_total',
);
$params['product_includes'] = array(
'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(),
'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;
}

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\SqlQuery;
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
*
* @override ReportsDataStore::__construct()
*/
public function __construct() {
$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.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_stats';
@@ -41,6 +44,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'orders';
@@ -48,6 +53,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -66,12 +73,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'orders';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @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;
// 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' => $this->date_column_name,
'before' => TimeInterval::default_before(),
'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(),
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/*
* We need to get the cache key here because
* parent::update_intervals_sql_params() modifies $query_args.
*/
$cache_key = $this->get_cache_key( $query_args );
$data = $this->get_cached_data( $cache_key );
if ( false === $data ) {
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$this->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' => 0,
'total' => $db_records_count,
'pages' => 0,
'page_no' => 0,
);
$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 );
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'],
);
return $data;
}
/**
* Normalizes order_by clause to match to SQL query.
*
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user.
* @return string
*/

View File

@@ -19,24 +19,32 @@
namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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
*/
public function get_data() {
$args = apply_filters( 'woocommerce_analytics_orders_query_args', $this->get_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 );
protected function get_default_query_vars() {
return \WC_Object_Query::get_default_query_vars();
}
}

View File

@@ -9,15 +9,19 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Orders\Stats;
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.
*
* @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.
@@ -26,6 +30,19 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
*/
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.
*
@@ -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.
* @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 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 ) {
$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 = parent::prepare_item_for_response( $report, $request );
/**
* 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 );
}
/**
* 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
*/
public function get_item_schema() {
$data_values = array(
protected function get_item_properties_schema() {
return array(
'net_revenue' => array(
'description' => __( 'Net sales.', 'woocommerce' ),
'type' => 'number',
@@ -199,104 +178,19 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'readonly' => true,
),
);
}
$segments = array(
'segments' => array(
'description' => __( 'Reports data grouped by segment condition.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'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 );
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['title'] = 'report_orders_stats';
// Products is not shown in intervals.
unset( $data_values['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,
),
),
),
),
),
);
unset( $schema['properties']['intervals']['items']['properties']['subtotals']['properties']['products'] );
return $this->add_additional_fields_schema( $schema );
}
@@ -307,69 +201,12 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['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 = parent::get_collection_params();
$params['orderby']['enum'] = array(
'date',
'net_revenue',
'orders_count',
'avg_order_value',
);
$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' ),
@@ -412,7 +249,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'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' ),
'type' => 'array',
'items' => array(
@@ -421,7 +258,8 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(),
'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' ),
'type' => 'array',
'items' => array(
@@ -431,7 +269,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'sanitize_callback' => 'wp_parse_id_list',
'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' ),
'type' => 'array',
'items' => array(
@@ -441,7 +279,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'validate_callback' => 'rest_validate_request_arg',
'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' ),
'type' => 'array',
'items' => array(
@@ -450,7 +288,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(),
'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' ),
'type' => 'array',
'items' => array(
@@ -459,7 +297,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(),
'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' ),
'type' => 'array',
'items' => array(
@@ -469,7 +307,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'sanitize_callback' => 'wp_parse_id_list',
'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' ),
'type' => 'array',
'items' => array(
@@ -479,7 +317,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['customer'] = array(
$params['customer'] = array(
'description' => __( 'Alias for customer_type (deprecated).', 'woocommerce' ),
'type' => 'string',
'enum' => array(
@@ -488,7 +326,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
),
'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' ),
'type' => 'string',
'enum' => array(
@@ -497,7 +335,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['refunds'] = array(
$params['refunds'] = array(
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce' ),
'type' => 'string',
'default' => '',
@@ -510,7 +348,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
),
'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' ),
'type' => 'array',
'items' => array(
@@ -519,7 +357,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(),
'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' ),
'type' => 'array',
'items' => array(
@@ -528,7 +366,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
$params['segmentby'] = array(
$params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'woocommerce' ),
'type' => 'string',
'enum' => array(
@@ -540,21 +378,8 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
),
'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['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',
);
unset( $params['intervals'] );
unset( $params['fields'] );
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\Customers\DataStore as CustomersDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/**
* API\Reports\Orders\Stats\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/**
* Table used to get the data.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_stats';
@@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'orders_stats';
@@ -42,6 +48,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Type for each column to cast values correctly later.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -65,12 +73,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'orders_stats';
/**
* Dynamically sets the date column name based on configuration
*
* @override ReportsDataStore::__construct()
*/
public function __construct() {
$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.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @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;
$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'] ) ) {
$this->date_column_name = $query_args['date_type'];
}
if ( false === $data ) {
$this->initialize_queries();
$this->initialize_queries();
$data = (object) array(
'totals' => (object) array(),
'intervals' => (object) array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$this->add_time_period_sql_params( $query_args, $table_name );
$this->add_intervals_sql_params( $query_args, $table_name );
$this->add_order_by_sql_params( $query_args );
$where_time = $this->get_sql_clause( 'where_time' );
$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 );
$this->add_time_period_sql_params( $query_args, $table_name );
$this->add_intervals_sql_params( $query_args, $table_name );
$this->add_order_by_sql_params( $query_args );
$where_time = $this->get_sql_clause( 'where_time' );
$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";
// 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 );
// 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(
// 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_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(
// 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;
}
@@ -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;
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
*/
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.
@@ -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 WP_REST_Request $request Request object.
* @param array $stat_data 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( $stat_data, $request ) {
@@ -527,8 +527,13 @@ class Controller extends GenericController {
*/
public function format_data_value( $data, $stat, $report, $chart, $query_args ) {
if ( 'jetpack/stats' === $report ) {
$index = false;
// Get the index of the field to tally.
$index = array_search( $chart, $data['general']->visits->fields, true );
if ( isset( $data['general']->visits->fields ) && is_array( $data['general']->visits->fields ) ) {
$index = array_search( $chart, $data['general']->visits->fields, true );
}
if ( ! $index ) {
return null;
}

View File

@@ -9,8 +9,9 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
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_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 ) {
$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 ];
}
}
}
$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
);
protected function get_datastore_data( $query_args = array() ) {
$query = new GenericQuery( $query_args, 'products' );
return $query->get_data();
}
/**
* 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.
* @return WP_REST_Response
*/
@@ -101,8 +73,36 @@ class Controller extends GenericController implements ExportableInterface {
* @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.
*
* @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.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_product_lookup';
@@ -28,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'products';
@@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -79,12 +85,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'products';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
*
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion.
* @return string
*/
@@ -256,122 +268,137 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Returns the report data based on parameters supplied by the user.
*
* @override ReportsDataStore::get_data()
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
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;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'product_includes' => array(),
'extended_info' => false,
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$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 );
$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 ( false === $data ) {
$this->initialize_queries();
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'] );
$data = (object) array(
'data' => array(),
'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();
if ( 'date' === $query_args['orderby'] ) {
$selections .= ", {$table_name}.date_created";
}
$product_data = $wpdb->get_results(
$products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
ARRAY_A
$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
);
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;
}
$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 );
$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();
}
$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;
}

View File

@@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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 {
/**
* Valid fields for Products report.
*
* @deprecated 9.3.0 Products\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array
*/
protected function get_default_query_vars() {
@@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -9,8 +9,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request;
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.
* @return array|WP_Error
* @override GenericController::get_datastore_data()
*
* @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(
'fields' => array(
'items_sold',
@@ -75,36 +88,13 @@ class Controller extends GenericStatsController {
}
}
$query = new Query( $query_args );
try {
$report_data = $query->get_data();
} catch ( ParameterException $e ) {
return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$out_data,
(int) $report_data->total,
(int) $report_data->page_no,
(int) $report_data->pages
);
return $query_args;
}
/**
* 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.
* @return WP_REST_Response
*/
@@ -255,15 +245,6 @@ class Controller extends GenericStatsController {
),
'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;
}

View File

@@ -8,18 +8,22 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products\Stats;
defined( 'ABSPATH' ) || exit;
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\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/**
* API\Reports\Products\Stats\DataStore.
*/
class DataStore extends ProductsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/**
* Mapping columns to data type to return correct response types.
*
* @override ProductsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -36,6 +40,8 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ProductsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'products_stats';
@@ -43,12 +49,16 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ProductsDataStore::$context
*
* @var string
*/
protected $context = 'products_stats';
/**
* Assign report columns once full table name has been assigned.
*
* @override ProductsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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' );
}
/**
* 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.
*
* @since 3.5.0
* @override ProductsDataStore::get_data()
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
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;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'interval' => 'week',
'product_includes' => array(),
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$this->update_sql_query_params( $query_args );
$this->get_limit_sql_params( $query_args );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
$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 );
/*
* 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 );
$db_interval_count = count( $db_intervals );
if ( false === $data ) {
$this->initialize_queries();
$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' ) );
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
$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
);
$this->update_sql_query_params( $query_args );
$this->get_limit_sql_params( $query_args );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
// phpcs: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' ),
'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(
$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 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 );
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(
// 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;
}
/**
* Normalizes order_by clause to match to SQL query.
*
* @override ProductsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user.
* @return string
*/
@@ -241,18 +254,4 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
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
*
* @deprecated 9.3.0 Products\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/
class Query extends ReportsQuery {
/**
* 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
*/
protected function get_default_query_vars() {
@@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -9,12 +9,15 @@ defined( 'ABSPATH' ) || exit;
/**
* 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 {
/**
* 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
*/
public function get_data() {

View File

@@ -16,12 +16,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Revenue;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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.

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\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request;
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.
* @return WP_REST_Response|WP_Error
* @override GenericController::get_datastore_data()
*
* @param array $query_args Query arguments.
* @return mixed Results from the data store.
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$reports_revenue = new RevenueQuery( $query_args );
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
);
protected function get_datastore_data( $query_args = array() ) {
$query = new RevenueQuery( $query_args );
return $query->get_data();
}
/**
@@ -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.
* @return WP_REST_Response
*/
@@ -279,6 +257,7 @@ class Controller extends GenericStatsController implements ExportableInterface {
),
'validate_callback' => 'rest_validate_request_arg',
);
unset( $params['fields'] );
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.
* @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.
* @return WP_REST_Response
*/

View File

@@ -18,6 +18,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* 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.
* @return array Array of counts.
*/

View File

@@ -10,12 +10,11 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Stock\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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.

View File

@@ -9,9 +9,10 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericController;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
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_Response;
@@ -34,6 +35,19 @@ class Controller extends GenericController implements ExportableInterface {
*/
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.
*
@@ -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.
* @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 mixed $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 ) {
$response = parent::prepare_item_for_response( $report, $request );
// Map to `object` for backwards compatibility.
$report = (object) $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.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_tax_lookup';
@@ -28,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'taxes';
@@ -35,6 +39,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -53,12 +59,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'taxes';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @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;
// 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' => 'tax_rate_id',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'taxes' => array(),
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$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 );
$this->add_sql_query_params( $query_args );
$params = $this->get_limit_params( $query_args );
if ( false === $data ) {
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
if ( isset( $query_args['taxes'] ) && is_array( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$total_results = count( $query_args['taxes'] );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
} else {
$db_records_count = (int) $wpdb->get_var(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- cache ok, DB call ok, unprepared SQL ok.
"SELECT COUNT(*) FROM ( {$this->subquery->get_query_statement()} ) AS tt"
);
$this->add_sql_query_params( $query_args );
$params = $this->get_limit_params( $query_args );
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( isset( $query_args['taxes'] ) && is_array( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
$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 ) {
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
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;
}
/**
* 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.
* @return string
*/

View File

@@ -21,12 +21,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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 {
/**
* Valid fields for Taxes report.
*
* @deprecated 9.3.0 Taxes\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array
*/
protected function get_default_query_vars() {
@@ -36,6 +40,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

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

View File

@@ -10,16 +10,19 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
use Automattic\WooCommerce\Admin\API\Reports\StatsDataStoreTrait;
/**
* API\Reports\Taxes\Stats\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/**
* Table used to get the data.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_tax_lookup';
@@ -27,6 +30,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
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.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -47,12 +54,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'taxes_stats';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$table_name = self::get_db_table_name();
@@ -107,12 +118,12 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public static function get_taxes( $args ) {
global $wpdb;
$query = "
SELECT
tax_rate_id,
tax_rate_country,
tax_rate_state,
tax_rate_name,
tax_rate_priority
SELECT
tax_rate_id,
tax_rate_country,
tax_rate_state,
tax_rate_name,
tax_rate_priority
FROM {$wpdb->prefix}woocommerce_tax_rates
";
if ( ! empty( $args['include'] ) ) {
@@ -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.
* @return stdClass|WP_Error Data.
* @override ReportsDataStore::get_default_query_vars()
*
* @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;
$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' => 'tax_rate_id',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'taxes' => array(),
$this->initialize_queries();
$selections = $this->selected_columns( $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(
// 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_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 );
$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' ) );
if ( false === $data ) {
$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(
'totals' => (object) array(),
'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 );
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_taxes_stats_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' => $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;
}
/**
* 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
*
* @deprecated 9.3.0 Taxes\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/
class Query extends ReportsQuery {
/**
* 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
*/
protected function get_default_query_vars() {
@@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -9,17 +9,24 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
use Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
use Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
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.
*
* @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.
*/
@@ -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();
/**
* 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
*
* @param array $collection_params List of parameters.
*
* @since 6.5.0
*/
$collection_params = apply_filters(
'experimental_woocommerce_analytics_variations_collection_params',
@@ -72,54 +120,7 @@ class Controller extends ReportsController implements ExportableInterface {
}
}
}
$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 );
return $args;
}
/**
@@ -244,38 +245,15 @@ class Controller extends ReportsController implements ExportableInterface {
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
$params = parent::get_collection_params();
$params['orderby']['enum'] = array(
'date',
'net_revenue',
'orders_count',
'items_sold',
'sku',
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['match'] = array(
$params['match'] = array(
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce' ),
'type' => 'string',
'default' => 'all',
@@ -285,27 +263,7 @@ class Controller extends ReportsController implements ExportableInterface {
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'net_revenue',
'orders_count',
'items_sold',
'sku',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['product_includes'] = array(
$params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -315,7 +273,7 @@ class Controller extends ReportsController implements ExportableInterface {
'sanitize_callback' => 'wp_parse_id_list',
'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' ),
'type' => 'array',
'items' => array(
@@ -325,7 +283,7 @@ class Controller extends ReportsController implements ExportableInterface {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
$params['variations'] = array(
$params['variations'] = array(
'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@@ -334,14 +292,14 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer',
),
);
$params['extended_info'] = array(
$params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each variation to the report.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_is'] = array(
$params['attribute_is'] = array(
'description' => __( 'Limit result set to variations that include the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -350,7 +308,7 @@ class Controller extends ReportsController implements ExportableInterface {
'default' => array(),
'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' ),
'type' => 'array',
'items' => array(
@@ -359,7 +317,7 @@ class Controller extends ReportsController implements ExportableInterface {
'default' => array(),
'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' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@@ -368,7 +326,7 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer',
),
);
$params['category_excludes'] = array(
$params['category_excludes'] = array(
'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@@ -377,13 +335,7 @@ class Controller extends ReportsController implements ExportableInterface {
'type' => 'integer',
),
);
$params['force_cache_refresh'] = array(
'description' => __( 'Force retrieval of fresh data instead of from the cache.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'wp_validate_boolean',
'validate_callback' => 'rest_validate_request_arg',
);
$params['products'] = array(
$params['products'] = array(
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
'type' => 'array',
'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\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
/**
@@ -20,6 +19,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Table used to get the data.
*
* @override ReportsDataStore::$table_name
*
* @var string
*/
protected static $table_name = 'wc_order_product_lookup';
@@ -27,6 +28,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override ReportsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'variations';
@@ -34,6 +37,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Mapping columns to data type to return correct response types.
*
* @override ReportsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -70,12 +75,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override ReportsDataStore::$context
*
* @var string
*/
protected $context = 'variations';
/**
* Assign report columns once full table name has been assigned.
*
* @override ReportsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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.
*
* @override ReportsDataStore::normalize_order_by()
*
* @param string $order_by Sorting criterion.
*
* @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;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'product_includes' => array(),
'variation_includes' => array(),
'extended_info' => false,
$this->initialize_queries();
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$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 );
$selections = $this->selected_columns( $query_args );
$included_variations =
( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) )
? $query_args['variation_includes']
: array();
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
if ( false === $data ) {
$this->initialize_queries();
if ( count( $included_variations ) > 0 ) {
$total_results = count( $included_variations );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
$selections = $this->selected_columns( $query_args );
$included_variations =
( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) )
? $query_args['variation_includes']
: array();
$params = $this->get_limit_params( $query_args );
$this->add_sql_query_params( $query_args );
if ( count( $included_variations ) > 0 ) {
$total_results = count( $included_variations );
$total_pages = (int) ceil( $total_results / $params['per_page'] );
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
if ( 'date' === $query_args['orderby'] ) {
$this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
}
$fields = $this->get_fields( $query_args );
$join_selections = $this->format_join_selections( $fields, array( 'variation_id' ) );
$ids_table = $this->get_ids_table( $included_variations, 'variation_id' );
$this->add_sql_clause( 'select', $join_selections );
$this->add_sql_clause( 'from', '(' );
$this->add_sql_clause( 'from', $this->subquery->get_query_statement() );
$this->add_sql_clause( 'from', ") AS {$table_name}" );
$this->add_sql_clause(
'right_join',
"RIGHT JOIN ( {$ids_table} ) AS default_results
ON default_results.variation_id = {$table_name}.variation_id"
);
$variations_query = $this->get_query_statement();
} else {
$this->subquery->clear_sql_clause( 'select' );
$this->subquery->add_sql_clause( 'select', $selections );
/**
* Experimental: Filter the Variations SQL query allowing extensions to add additional SQL clauses.
*
* @since 7.4.0
* @param array $query_args Query parameters.
* @param SqlQuery $subquery Variations query class.
*/
apply_filters( 'experimental_woocommerce_analytics_variations_additional_clauses', $query_args, $this->subquery );
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
{$this->subquery->get_query_statement()}
) AS tt"
);
/* phpcs:enable */
$total_results = $db_records_count;
$total_pages = (int) ceil( $db_records_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$variations_query = $this->subquery->get_query_statement();
if ( 'date' === $query_args['orderby'] ) {
$this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
}
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$product_data = $wpdb->get_results(
$variations_query,
ARRAY_A
$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 */
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;
}
$this->include_extended_info( $product_data, $query_args );
if ( $query_args['extended_info'] ) {
$this->fill_deleted_product_name( $product_data );
}
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
$data = (object) array(
'data' => $product_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
$this->set_cached_data( $cache_key, $data );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$variations_query = $this->subquery->get_query_statement();
}
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$product_data = $wpdb->get_results(
$variations_query,
ARRAY_A
);
/* phpcs:enable */
if ( null === $product_data ) {
return $data;
}
$this->include_extended_info( $product_data, $query_args );
if ( $query_args['extended_info'] ) {
$this->fill_deleted_product_name( $product_data );
}
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
$data = (object) array(
'data' => $product_data,
'total' => $total_results,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
return $data;
}

View File

@@ -22,12 +22,16 @@ use Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
/**
* 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 {
/**
* Valid fields for Products report.
*
* @deprecated 9.3.0 Variations\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*
* @return array
*/
protected function get_default_query_vars() {
@@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -9,8 +9,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\API\Reports\GenericQuery;
use Automattic\WooCommerce\Admin\API\Reports\GenericStatsController;
use Automattic\WooCommerce\Admin\API\Reports\ParameterException;
use WP_REST_Request;
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.
* @return array|WP_Error
* @override GenericController::get_datastore_data()
*
* @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(
'fields' => array(
'items_sold',
@@ -79,36 +92,13 @@ class Controller extends GenericStatsController {
}
}
$query = new Query( $query_args );
try {
$report_data = $query->get_data();
} catch ( ParameterException $e ) {
return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
return $this->add_pagination_headers(
$request,
$out_data,
(int) $report_data->total,
(int) $report_data->page_no,
(int) $report_data->pages
);
return $query_args;
}
/**
* 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.
* @return WP_REST_Response
*/
@@ -288,15 +278,6 @@ class Controller extends GenericStatsController {
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['fields'] = array(
'description' => __( 'Limit stats fields to the specified items.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
$params['attribute_is'] = array(
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',

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\DataStoreInterface;
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.
*/
class DataStore extends VariationsDataStore implements DataStoreInterface {
use StatsDataStoreTrait;
/**
* Mapping columns to data type to return correct response types.
*
* @override VariationsDataStore::$column_types
*
* @var array
*/
protected $column_types = array(
@@ -32,6 +35,8 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
/**
* Cache identifier.
*
* @override VariationsDataStore::$cache_key
*
* @var string
*/
protected $cache_key = 'variations_stats';
@@ -39,12 +44,16 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
/**
* Data store context used to pass to filters.
*
* @override VariationsDataStore::$context
*
* @var string
*/
protected $context = 'variations_stats';
/**
* Assign report columns once full table name has been assigned.
*
* @override VariationsDataStore::assign_report_columns()
*/
protected function assign_report_columns() {
$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
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
* @override VariationsDataStore::get_default_query_vars()
*
* @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;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => TimeInterval::default_before(),
'after' => TimeInterval::default_after(),
'fields' => '*',
'category_includes' => array(),
'interval' => 'week',
'product_includes' => array(),
'variation_includes' => array(),
$this->initialize_queries();
$selections = $this->selected_columns( $query_args );
$this->update_sql_query_params( $query_args );
$this->get_limit_sql_params( $query_args );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
/* phpcs:enable */
/*
* 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 );
$db_interval_count = count( $db_intervals );
if ( false === $data ) {
$this->initialize_queries();
$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' ) );
$selections = $this->selected_columns( $query_args );
$params = $this->get_limit_params( $query_args );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
$this->update_sql_query_params( $query_args );
$this->get_limit_sql_params( $query_args );
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
// phpcs: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' ),
'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 */
$db_intervals = $wpdb->get_col(
$this->interval_query->get_query_statement()
);
/* phpcs:enable */
$db_interval_count = count( $db_intervals );
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$intervals = array();
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->total_query->add_sql_clause( 'select', $selections );
$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$totals = $wpdb->get_results(
$this->total_query->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
// @todo remove these assignements when refactoring segmenter classes to use query objects.
$totals_query = array(
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
);
$intervals_query = array(
'select_clause' => $this->get_sql_clause( 'select' ),
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
'order_by' => $this->get_sql_clause( 'order_by' ),
'limit' => $this->get_sql_clause( 'limit' ),
);
$segmenter = new Segmenter( $query_args, $this->report_columns );
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
if ( null === $totals ) {
return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
/* phpcs:disable WordPress.DB.PreparedSQL.NotPrepared */
$intervals = $wpdb->get_results(
$this->interval_query->get_query_statement(),
ARRAY_A
);
/* phpcs:enable */
if ( null === $intervals ) {
return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$totals = (object) $this->cast_numbers( $totals[0] );
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
} else {
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
}
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
$this->create_interval_subtotals( $data->intervals );
$this->set_cached_data( $cache_key, $data );
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->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;
}
/**
* Normalizes order_by clause to match to SQL query.
*
* @override VariationsDataStore::normalize_order_by()
*
* @param string $order_by Order by option requeste by user.
* @return string
*/
@@ -281,18 +277,4 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
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
*
* @deprecated 9.3.0 Variations\Stats\Query class is deprecated, please use GenericQuery or \WC_Object_Query instead.
*/
class Query extends ReportsQuery {
/**
* 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
*/
protected function get_default_query_vars() {
@@ -37,6 +41,8 @@ class Query extends ReportsQuery {
/**
* 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
*/
public function get_data() {

View File

@@ -0,0 +1,59 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Exporters;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Exporters\HasAlias;
use Automattic\WooCommerce\Blueprint\Steps\SetSiteOptions;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* ExportWCCoreProfilerOptions class
*/
class ExportWCCoreProfilerOptions implements StepExporter, HasAlias {
use UseWPFunctions;
/**
* Export the step
*
* @return SetSiteOptions
*/
public function export() {
$step = new SetSiteOptions(
array(
'blogname' => $this->wp_get_option( 'blogname' ),
'woocommerce_allow_tracking' => $this->wp_get_option( 'woocommerce_allow_tracking' ),
'woocommerce_onboarding_profile' => $this->wp_get_option( 'woocommerce_onboarding_profile', array() ),
'woocommerce_default_country' => $this->wp_get_option( 'woocommerce_default_country' ),
)
);
$step->set_meta_values(
array(
'plugin' => 'woocommerce',
'alias' => $this->get_alias(),
)
);
return $step;
}
/**
* Get the step name
*
* @return string
*/
public function get_step_name() {
return 'setSiteOptions';
}
/**
* Get the alias
*
* @return string
*/
public function get_alias() {
return 'setWCCoreProfilerOptions';
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Exporters;
use Automattic\WooCommerce\Admin\Features\Blueprint\Steps\SetWCPaymentGateways;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Steps\Step;
/**
* ExportWCPaymentGateways class
*/
class ExportWCPaymentGateways implements StepExporter {
/**
* Payment gateway IDs to exclude from export
*
* @var array|string[] Payment gateway IDs to exclude from export
*/
protected array $exclude_ids = array( 'pre_install_woocommerce_payments_promotion' );
/**
* Export the step
*
* @return Step
*/
public function export(): Step {
$step = new SetWCPaymentGateways();
$this->maybe_hide_wcpay_gateways();
foreach ( $this->get_wc_payment_gateways() as $id => $payment_gateway ) {
if ( in_array( $id, $this->exclude_ids, true ) ) {
continue;
}
$step->add_payment_gateway(
$id,
$payment_gateway->get_title(),
$payment_gateway->get_description(),
$payment_gateway->is_available() ? 'yes' : 'no'
);
}
return $step;
}
/**
* Return the payment gateways resgietered in WooCommerce
*
* @return string
*/
public function get_wc_payment_gateways() {
return WC()->payment_gateways->payment_gateways();
}
/**
* Get the step name
*
* @return string
*/
public function get_step_name() {
return SetWCPaymentGateways::get_step_name();
}
/**
* Maybe hide WooCommerce Payments gateways
*
* @return void
*/
protected function maybe_hide_wcpay_gateways() {
if ( class_exists( 'WC_Payments' ) ) {
\WC_Payments::hide_gateways_on_settings_page();
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Exporters;
use Automattic\WooCommerce\Blueprint\Exporters\HasAlias;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Steps\SetSiteOptions;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
use Automattic\WooCommerce\Blueprint\Util;
use WC_Admin_Settings;
use WC_Settings_Page;
/**
* Class ExportWCSettings
*
* This class exports WooCommerce settings and implements the StepExporter and HasAlias interfaces.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Exporters
*/
class ExportWCSettings implements StepExporter, HasAlias {
use UseWPFunctions;
/**
* Array of WC_Settings_Page objects.
*
* @var WC_Settings_Page[]
*/
private array $setting_pages;
/**
* Array of page IDs to exclude from export.
*
* @var array
*/
private array $exclude_pages = array( 'integration', 'site-visibility' );
/**
* Constructor.
*
* @param array $setting_pages Optional array of setting pages.
*/
public function __construct( array $setting_pages = array() ) {
if ( empty( $setting_pages ) ) {
$setting_pages = WC_Admin_Settings::get_settings_pages();
}
$this->setting_pages = $setting_pages;
$this->wp_add_filter( 'wooblueprint_export_settings', array( $this, 'add_site_visibility_settings' ), 10, 3 );
}
/**
* Export WooCommerce settings.
*
* @return SetSiteOptions
*/
public function export() {
$pages = array();
$options = array();
$option_info = array();
foreach ( $this->setting_pages as $page ) {
$id = $page->get_id();
if ( in_array( $id, $this->exclude_pages, true ) ) {
continue;
}
$pages[ $id ] = $this->get_page_info( $page );
foreach ( $pages[ $id ]['options'] as $option ) {
$options[ $option['id'] ] = $option['value'];
$option_info[ $option['id'] ] = array(
'location' => $option['location'],
'title' => $option['title'],
);
}
unset( $pages[ $id ]['options'] );
}
$filtered = $this->wp_apply_filters( 'wooblueprint_export_settings', $options, $pages, $option_info );
$step = new SetSiteOptions( $filtered['options'] );
$step->set_meta_values(
array(
'plugin' => 'woocommerce',
'pages' => $filtered['pages'],
'info' => $option_info,
'alias' => $this->get_alias(),
)
);
return $step;
}
/**
* Get information about a settings page.
*
* @param WC_Settings_Page $page The settings page.
* @return array
*/
protected function get_page_info( WC_Settings_Page $page ) {
$info = array(
'label' => $page->get_label(),
'sections' => array(),
);
foreach ( $page->get_sections() as $id => $section ) {
$section_id = Util::camel_to_snake( strtolower( $section ) );
$info['sections'][ $section_id ] = array(
'label' => $section,
'subsections' => array(),
);
$settings = $page->get_settings_for_section( $id );
// Get subsections.
$subsections = array_filter(
$settings,
function ( $setting ) {
return isset( $setting['type'] ) && 'title' === $setting['type'] && isset( $setting['title'] );
}
);
foreach ( $subsections as $subsection ) {
if ( ! isset( $subsection['id'] ) ) {
$subsection['id'] = Util::camel_to_snake( strtolower( $subsection['title'] ) );
}
$info['sections'][ $section_id ]['subsections'][ $subsection['id'] ] = array(
'label' => $subsection['title'],
);
}
// Get options.
$info['options'] = $this->get_page_section_settings( $settings, $page->get_id(), $section_id );
}
return $info;
}
/**
* Get settings for a specific page section.
*
* @param array $settings The settings.
* @param string $page The page ID.
* @param string $section The section ID.
* @return array
*/
private function get_page_section_settings( $settings, $page, $section = '' ) {
$current_title = '';
$data = array();
foreach ( $settings as $setting ) {
if ( 'sectionend' === $setting['type'] || 'slotfill_placeholder' === $setting['type'] || ! isset( $setting['id'] ) ) {
continue;
}
if ( 'title' === $setting['type'] ) {
$current_title = Util::camel_to_snake( strtolower( $setting['title'] ) );
} else {
$location = $page . '.' . $section;
if ( $current_title ) {
$location .= '.' . $current_title;
}
$data[] = array(
'id' => $setting['id'],
'value' => $this->wp_get_option( $setting['id'], $setting['default'] ?? null ),
'title' => $setting['title'] ?? $setting['desc'] ?? '',
'location' => $location,
);
}
}
return $data;
}
/**
* Add site visibility settings.
*
* @param array $options The options array.
* @param array $pages The pages array.
* @param array $option_info The option information array.
* @return array
*/
public function add_site_visibility_settings( array $options, array $pages, array $option_info ) {
$pages['site_visibility'] = array(
'label' => 'Site Visibility',
'sections' => array(
'general' => array(
'label' => 'General',
),
),
);
$options['woocommerce_coming_soon'] = $this->wp_get_option( 'woocommerce_coming_soon' );
$options['woocommerce_store_pages_only'] = $this->wp_get_option( 'woocommerce_store_pages_only' );
$option_info['woocommerce_coming_soon'] = array(
'location' => 'site_visibility.general',
'title' => 'Coming soon',
);
$option_info['woocommerce_store_pages_only'] = array(
'location' => 'site_visibility.general',
'title' => 'Apply to store pages only',
);
return compact( 'options', 'pages', 'option_info' );
}
/**
* Get the name of the step.
*
* @return string
*/
public function get_step_name() {
return 'setSiteOptions';
}
/**
* Get the alias for this exporter.
*
* @return string
*/
public function get_alias() {
return 'setWCSettings';
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Exporters;
use Automattic\WooCommerce\Admin\Features\Blueprint\Steps\SetWCShipping;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Util;
/**
* Class ExportWCShipping
*
* This class exports WooCommerce shipping settings and implements the StepExporter interface.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Exporters
*/
class ExportWCShipping implements StepExporter {
/**
* Export WooCommerce shipping settings.
*
* @return SetWCShipping
*/
public function export() {
global $wpdb;
// Fetch shipping classes from the database.
$classes = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}term_taxonomy
WHERE taxonomy = 'product_shipping_class'
"
);
$term_ids = array();
// Collect term IDs.
foreach ( $classes as $term ) {
$term_ids[] = (int) $term->term_id;
}
$term_ids = implode( ', ', $term_ids );
// Fetch terms based on term IDs.
if ( ! empty( $term_ids ) ) {
$terms = $wpdb->get_results(
$wpdb->prepare(
"
SELECT *
FROM {$wpdb->prefix}terms
WHERE term_id IN (%s)
",
$term_ids
)
);
} else {
$terms = array();
}
// Fetch local pickup settings.
$local_pickup = array(
'general' => get_option( 'woocommerce_pickup_location_settings', array() ),
'locations' => get_option( 'pickup_location_pickup_locations', array() ),
);
if ( empty( $local_pickup['general'] ) ) {
$local_pickup['general'] = new \stdClass();
}
// Fetch shipping zones from the database.
$zones = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_shipping_zones
"
);
// Fetch shipping zone methods from the database.
$methods = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_shipping_zone_methods
"
);
// Fetch shipping method options.
// Each method has a corresponding option in the options table.
$method_options = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}options
WHERE option_name LIKE 'woocommerce_flat_rate_%_settings'
or option_name LIKE 'woocommerce_free_shipping_%_settings'
",
ARRAY_A
);
$method_options = Util::index_array(
$method_options,
function ( $key, $option ) {
return $option['option_name'];
}
);
foreach ( $methods as $method ) {
$key_name = 'woocommerce_' . $method->method_id . '_' . $method->instance_id . '_settings';
if ( isset( $method_options[ $key_name ] ) ) {
$method->settings = array(
'option_name' => $key_name,
'option_value' => maybe_unserialize( $method_options[ $key_name ]['option_value'] ),
);
}
}
$methods_by_zone_id = array();
// Organize methods by zone ID.
foreach ( $methods as $method ) {
if ( ! isset( $methods_by_zone_id[ $method->zone_id ] ) ) {
$methods_by_zone_id[ $method->zone_id ] = array();
}
$methods_by_zone_id[ $method->zone_id ][] = $method->method_id;
}
// Fetch shipping zone locations from the database.
$locations = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_shipping_zone_locations
"
);
$locations_by_zone_id = array();
// Organize locations by zone ID.
foreach ( $locations as $location ) {
if ( ! isset( $locations_by_zone_id[ $location->zone_id ] ) ) {
$locations_by_zone_id[ $location->zone_id ] = array();
}
$locations_by_zone_id[ $location->zone_id ][] = $location->location_id;
}
// Create a new SetWCShipping step with the fetched data.
$step = new SetWCShipping( $methods, $locations, $zones, $terms, $classes, $local_pickup );
$step->set_meta_values(
array(
'plugin' => 'woocommerce',
)
);
return $step;
}
/**
* Get the name of the step.
*
* @return string
*/
public function get_step_name() {
return SetWCShipping::get_step_name();
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Exporters;
use Automattic\WooCommerce\Blueprint\Exporters\ExportsStep;
use Automattic\WooCommerce\Blueprint\Exporters\HasAlias;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\Steps\SetSiteOptions;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ExportWCTaskOptions
*
* This class exports WooCommerce task options and implements the StepExporter and HasAlias interfaces.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Exporters
*/
class ExportWCTaskOptions implements StepExporter, HasAlias {
use UseWPFunctions;
/**
* Export WooCommerce task options.
*
* @return SetSiteOptions
*/
public function export() {
$step = new SetSiteOptions(
array(
'woocommerce_admin_customize_store_completed' => $this->wp_get_option( 'woocommerce_admin_customize_store_completed', 'no' ),
'woocommerce_task_list_tracked_completed_actions' => $this->wp_get_option( 'woocommerce_task_list_tracked_completed_actions', array() ),
)
);
$step->set_meta_values(
array(
'plugin' => 'woocommerce',
'alias' => $this->get_alias(),
)
);
return $step;
}
/**
* Get the name of the step.
*
* @return string
*/
public function get_step_name() {
return 'setOptions';
}
/**
* Get the alias for this exporter.
*
* @return string
*/
public function get_alias() {
return 'setWCTaskOptions';
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Exporters;
use Automattic\WooCommerce\Admin\Features\Blueprint\Steps\SetWCTaxRates;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
/**
* Class ExportWCTaxRates
*
* This class exports WooCommerce tax rates and implements the StepExporter interface.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Exporters
*/
class ExportWCTaxRates implements StepExporter {
/**
* Export WooCommerce tax rates.
*
* @return SetWCTaxRates
*/
public function export() {
global $wpdb;
// Fetch tax rates from the database.
$rates = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates
",
ARRAY_A
);
// Fetch tax rate locations from the database.
$locations = $wpdb->get_results(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_tax_rate_locations as locations
",
ARRAY_A
);
// Create a new SetWCTaxRates step with the fetched data.
$step = new SetWCTaxRates( $rates, $locations );
$step->set_meta_values(
array(
'plugin' => 'woocommerce',
)
);
return $step;
}
/**
* Get the name of the step.
*
* @return string
*/
public function get_step_name() {
return 'setWCTaxRates';
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Importers;
use Automattic\WooCommerce\Admin\Features\Blueprint\Steps\SetWCPaymentGateways;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/**
* Class ImportSetWCPaymentGateways
*
* This class imports WooCommerce payment gateways settings and implements the StepProcessor interface.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Importers
*/
class ImportSetWCPaymentGateways implements StepProcessor {
use UseWPFunctions;
/**
* Process the import of WooCommerce payment gateways settings.
*
* @param object $schema The schema object containing import details.
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( SetWCPaymentGateways::get_step_name() );
$payment_gateways = $this->get_wc_payment_gateways();
$fields = array( 'title', 'description', 'enabled' );
foreach ( $schema->payment_gateways as $id => $payment_gateway_data ) {
if ( ! isset( $payment_gateways[ $id ] ) ) {
$result->add_info( "Skipping {$id}. The payment gateway is not available" );
continue;
}
$payment_gateway = $payment_gateways[ $id ];
// Refer to https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/class-wc-ajax.php#L3564.
foreach ( $fields as $field ) {
if ( isset( $payment_gateway_data->{$field} ) ) {
$payment_gateway->update_option( $field, $payment_gateway_data->{$field} );
}
}
$result->add_info( "{$id} has been updated." );
$this->wp_do_action( 'woocommerce_update_options' );
}
return $result;
}
/**
* Return the payment gateways resgietered in WooCommerce
*
* @return string
*/
public function get_wc_payment_gateways() {
return WC()->payment_gateways->payment_gateways();
}
/**
* Get the class name for the step.
*
* @return string
*/
public function get_step_class(): string {
return SetWCPaymentGateways::class;
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Importers;
use Automattic\WooCommerce\Admin\Features\Blueprint\Steps\SetWCShipping;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;
use WC_Tax;
/**
* Class ImportSetWCShipping
*
* This class imports WooCommerce shipping settings and implements the StepProcessor interface.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Importers
*/
class ImportSetWCShipping implements StepProcessor {
use UseWPFunctions;
/**
* Process the import of WooCommerce shipping settings.
*
* @param object $schema The schema object containing import details.
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$result = StepProcessorResult::success( SetWCShipping::get_step_name() );
$fields = array(
'terms' => array( 'terms', array( '%d', '%s', '%s', '%d' ) ),
'classes' => array( 'term_taxonomy', array( '%d', '%d', '%s', '%s', '%d', '%d' ) ),
'shipping_zones' => array( 'woocommerce_shipping_zones', array( '%d', '%s', '%d' ) ),
'shipping_methods' => array( 'woocommerce_shipping_zone_methods', array( '%d', '%d', '%s', '%d', '%d' ) ),
'shipping_locations' => array( 'woocommerce_shipping_zone_locations', array( '%d', '%d', '%s', '%s' ) ),
);
foreach ( $fields as $name => $data ) {
if ( isset( $schema->values->{$name} ) ) {
$filter_method = 'filter_' . $name . '_data';
if ( method_exists( $this, $filter_method ) ) {
$insert_values = $this->$filter_method( $schema->values->{$name} );
} else {
$insert_values = $schema->values->{$name};
}
$this->insert( $data[0], $data[1], $insert_values );
// check if function with process_$name exist and call it.
$method = 'post_process_' . $name;
if ( method_exists( $this, $method ) ) {
$this->$method( $schema->values->{$name} );
}
}
}
if ( isset( $schema->values->local_pickup ) ) {
$this->add_local_pickup( $schema->values->local_pickup );
}
return $result;
}
/**
* Filter shipping methods data.
*
* @param array $methods The shipping methods.
*
* @return mixed
*/
protected function filter_shipping_methods_data( $methods ) {
return array_map(
function ( $method ) {
unset( $method->settings );
return $method;
},
$methods
);
}
/**
* Post process shipping methods.
*
* @param array $methods The shipping methods.
*
* @return void
*/
protected function post_process_shipping_methods( $methods ) {
foreach ( $methods as $method ) {
if ( isset( $method->settings ) ) {
update_option( $method->option_name, $method->option_value );
}
}
}
/**
* Insert data into the specified table.
*
* @param string $table The table name.
* @param array $format The data format.
* @param array $rows The rows to insert.
* @global \wpdb $wpdb WordPress database abstraction object.
* @return array The IDs of the inserted rows.
*/
protected function insert( $table, $format, $rows ) {
global $wpdb;
$inserted_ids = array();
$table = $wpdb->prefix . $table;
$format = implode( ', ', $format );
foreach ( $rows as $row ) {
$row = (array) $row;
$columns = implode( ', ', array_keys( $row ) );
// phpcs:ignore
$sql = $wpdb->prepare( "REPLACE INTO $table ($columns) VALUES ($format)", $row );
// phpcs:ignore
$wpdb->query( $sql );
}
return $inserted_ids;
}
/**
* Add local pickup settings.
*
* @param object $local_pickup The local pickup settings.
*/
private function add_local_pickup( $local_pickup ) {
if ( isset( $local_pickup->general ) ) {
$this->wp_update_option( 'woocommerce_pickup_location_settings', (array) $local_pickup->general );
}
if ( isset( $local_pickup->locations ) ) {
$local_pickup->locations = json_decode( wp_json_encode( $local_pickup->locations ), true );
$this->wp_update_option( 'pickup_location_pickup_locations', $local_pickup->locations );
}
}
/**
* Get the class name for the step.
*
* @return string
*/
public function get_step_class(): string {
return SetWCShipping::class;
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Importers;
use Automattic\WooCommerce\Admin\Features\Blueprint\Steps\SetWCTaxRates;
use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use WC_Tax;
/**
* Class ImportSetWCTaxRates
*
* This class imports WooCommerce tax rates and implements the StepProcessor interface.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Importers
*/
class ImportSetWCTaxRates implements StepProcessor {
/**
* The result of the step processing.
*
* @var StepProcessorResult $result The result of the step processing.
*/
private StepProcessorResult $result;
/**
* Process the import of WooCommerce tax rates.
*
* @param object $schema The schema object containing import details.
* @return StepProcessorResult
*/
public function process( $schema ): StepProcessorResult {
$this->result = StepProcessorResult::success( SetWCTaxRates::get_step_name() );
foreach ( $schema->values->rates as $rate ) {
$this->add_rate( $rate );
}
foreach ( $schema->values->locations as $location ) {
$this->add_location( $location );
}
return $this->result;
}
/**
* Check if a tax rate exists in the database.
*
* @param int $id The tax rate ID.
* @global \wpdb $wpdb WordPress database abstraction object.
* @return array|null The tax rate row if found, null otherwise.
*/
protected function exist( $id ) {
global $wpdb;
return $wpdb->get_row(
$wpdb->prepare(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_tax_rates
WHERE tax_rate_id = %d
",
$id
),
ARRAY_A
);
}
/**
* Add a tax rate to the database.
*
* @param object $rate The tax rate object.
* @return int|false The tax rate ID if successfully added, false otherwise.
*/
protected function add_rate( $rate ) {
$tax_rate = (array) $rate;
if ( $this->exist( $tax_rate['tax_rate_id'] ) ) {
$this->result->add_info( "Tax rate with I.D {$tax_rate['tax_rate_id']} already exists. Skipped creating it." );
return false;
}
$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
if ( isset( $rate->postcode ) ) {
$postcode = array_map( 'wc_clean', explode( ';', $rate->postcode ) );
$postcode = array_map( 'wc_normalize_postcode', $postcode );
WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, $postcode );
}
if ( isset( $rate->city ) ) {
$cities = explode( ';', $rate->city );
WC_Tax::_update_tax_rate_cities( $tax_rate_id, array_map( 'wc_clean', array_map( 'wp_unslash', $cities ) ) );
}
return $tax_rate_id;
}
/**
* Add a tax rate location to the database.
*
* @param object $location The location object.
* @global \wpdb $wpdb WordPress database abstraction object.
*/
public function add_location( $location ) {
global $wpdb;
$location = (array) $location;
$columns = implode( ',', array_keys( $location ) );
$format = implode( ',', array( '%d', '%s', '%d', '%s' ) );
$table = $wpdb->prefix . 'woocommerce_tax_rate_locations';
// phpcs:ignore
$sql = $wpdb->prepare( "REPLACE INTO $table ($columns) VALUES ($format)", $location );
// phpcs:ignore
$wpdb->query( $sql );
}
/**
* Get the class name for the step.
*
* @return string
*/
public function get_step_class(): string {
return SetWCTaxRates::class;
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\Features\Blueprint;
use Automattic\WooCommerce\Admin\Features\Blueprint\Exporters\ExportWCCoreProfilerOptions;
use Automattic\WooCommerce\Admin\Features\Blueprint\Exporters\ExportWCPaymentGateways;
use Automattic\WooCommerce\Admin\Features\Blueprint\Exporters\ExportWCSettings;
use Automattic\WooCommerce\Admin\Features\Blueprint\Exporters\ExportWCShipping;
use Automattic\WooCommerce\Admin\Features\Blueprint\Exporters\ExportWCTaskOptions;
use Automattic\WooCommerce\Admin\Features\Blueprint\Exporters\ExportWCTaxRates;
use Automattic\WooCommerce\Admin\Features\Blueprint\Importers\ImportSetWCPaymentGateways;
use Automattic\WooCommerce\Admin\Features\Blueprint\Importers\ImportSetWCShipping;
use Automattic\WooCommerce\Admin\Features\Blueprint\Importers\ImportSetWCTaxRates;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Blueprint\Exporters\StepExporter;
use Automattic\WooCommerce\Blueprint\StepProcessor;
/**
* Class Init
*
* This class initializes the Blueprint feature for WooCommerce.
*/
class Init {
/**
* Init constructor.
*/
public function __construct() {
add_action( 'rest_api_init', array( $this, 'init_rest_api' ) );
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'add_upload_nonce_to_settings' ) );
add_filter(
'wooblueprint_export_landingpage',
function () {
return 'admin.php?page=wc-admin';
}
);
add_filter( 'wooblueprint_exporters', array( $this, 'add_woo_exporters' ) );
add_filter( 'wooblueprint_importers', array( $this, 'add_woo_importers' ) );
}
/**
* Register REST API routes.
*
* @return void
*/
public function init_rest_api() {
( new RestApi() )->register_routes();
}
/**
* Add upload nonce to global JS settings.
*
* The value can be accessed at wcSettings.admin.blueprint_upload_nonce
*
* @param array $settings Global JS settings.
*
* @return array
*/
public function add_upload_nonce_to_settings( array $settings ) {
if ( ! is_admin() ) {
return $settings;
}
$page_id = PageController::get_instance()->get_current_screen_id();
if ( 'woocommerce_page_wc-admin' === $page_id ) {
$settings['blueprint_upload_nonce'] = wp_create_nonce( 'blueprint_upload_nonce' );
return $settings;
}
return $settings;
}
/**
* Add Woo Specific Exporters.
*
* @param StepExporter[] $exporters Array of step exporters.
*
* @return StepExporter[]
*/
public function add_woo_exporters( array $exporters ) {
return array_merge(
$exporters,
array(
new ExportWCCoreProfilerOptions(),
new ExportWCSettings(),
new ExportWCPaymentGateways(),
new ExportWCShipping(),
new ExportWCTaskOptions(),
new ExportWCTaxRates(),
)
);
}
/**
* Add Woo Specific Importers.
*
* @param StepProcessor[] $importers Array of step processors.
*
* @return array
*/
public function add_woo_importers( array $importers ) {
return array_merge(
$importers,
array(
new ImportSetWCPaymentGateways(),
new ImportSetWCShipping(),
new ImportSetWCTaxRates(),
)
);
}
}

View File

@@ -0,0 +1,213 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Admin\Features\Blueprint;
use Automattic\WooCommerce\Blueprint\ExportSchema;
use Automattic\WooCommerce\Blueprint\ImportSchema;
use Automattic\WooCommerce\Blueprint\JsonResultFormatter;
use Automattic\WooCommerce\Blueprint\ZipExportedSchema;
/**
* Class RestApi
*
* This class handles the REST API endpoints for importing and exporting WooCommerce Blueprints.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint
*/
class RestApi {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'blueprint';
/**
* Register routes.
*
* @since 9.3.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/import',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'import' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
register_rest_route(
$this->namespace,
'/export',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'export' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => array(
'steps' => array(
'description' => __( 'A list of plugins to install', 'woocommerce' ),
'type' => 'array',
'items' => 'string',
'default' => array(),
'sanitize_callback' => function ( $value ) {
return array_map(
function ( $value ) {
return sanitize_text_field( $value );
},
$value
);
},
'required' => false,
),
'export_as_zip' => array(
'description' => __( 'Export as a zip file', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'required' => false,
),
),
),
)
);
}
/**
* Check if the current user has permission to perform the request.
*
* @return bool|\WP_Error
*/
public function check_permission() {
if ( ! current_user_can( 'install_plugins' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Handle the export request.
*
* @param \WP_REST_Request $request The request object.
* @return \WP_HTTP_Response The response object.
*/
public function export( $request ) {
$steps = $request->get_param( 'steps' );
$export_as_zip = $request->get_param( 'export_as_zip' );
$exporter = new ExportSchema();
$data = $exporter->export( $steps, $export_as_zip );
if ( $export_as_zip ) {
$zip = new ZipExportedSchema( $data );
$data = $zip->zip();
$data = site_url( str_replace( ABSPATH, '', $data ) );
}
return new \WP_HTTP_Response(
array(
'data' => $data,
'type' => $export_as_zip ? 'zip' : 'json',
)
);
}
/**
* Handle the import request.
*
* @return \WP_HTTP_Response The response object.
* @throws \InvalidArgumentException If the import fails.
*/
public function import() {
// Check for nonce to prevent CSRF.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! isset( $_POST['blueprint_upload_nonce'] ) || ! \wp_verify_nonce( $_POST['blueprint_upload_nonce'], 'blueprint_upload_nonce' ) ) {
return new \WP_HTTP_Response(
array(
'status' => 'error',
'message' => __( 'Invalid nonce', 'woocommerce' ),
),
400
);
}
// phpcs:ignore
if ( ! empty( $_FILES['file'] ) && $_FILES['file']['error'] === UPLOAD_ERR_OK ) {
// phpcs:ignore
$uploaded_file = $_FILES['file']['tmp_name'];
// phpcs:ignore
$mime_type = $_FILES['file']['type'];
if ( 'application/json' !== $mime_type && 'application/zip' !== $mime_type ) {
return new \WP_HTTP_Response(
array(
'status' => 'error',
'message' => __( 'Invalid file type', 'woocommerce' ),
),
400
);
}
try {
// phpcs:ignore
if ( $mime_type === 'application/zip' ) {
// phpcs:ignore
if ( ! function_exists( 'wp_handle_upload' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$movefile = \wp_handle_upload( $_FILES['file'], array( 'test_form' => false ) );
if ( $movefile && ! isset( $movefile['error'] ) ) {
$blueprint = ImportSchema::create_from_zip( $movefile['file'] );
} else {
throw new InvalidArgumentException( $movefile['error'] );
}
} else {
$blueprint = ImportSchema::create_from_json( $uploaded_file );
}
} catch ( \Exception $e ) {
return new \WP_HTTP_Response(
array(
'status' => 'error',
'message' => $e->getMessage(),
),
400
);
}
$results = $blueprint->import();
$result_formatter = new JsonResultFormatter( $results );
$redirect = $blueprint->get_schema()->landingPage ?? null;
$redirect_url = $redirect->url ?? 'admin.php?page=wc-admin';
$is_success = $result_formatter->is_success() ? 'success' : 'error';
return new \WP_HTTP_Response(
array(
'status' => $is_success,
'message' => 'error' === $is_success ? __( 'There was an error while processing your schema', 'woocommerce' ) : 'success',
'data' => array(
'redirect' => admin_url( $redirect_url ),
'result' => $result_formatter->format(),
),
),
200
);
}
return new \WP_HTTP_Response(
array(
'status' => 'error',
'message' => __( 'No file uploaded', 'woocommerce' ),
),
400
);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Steps;
use Automattic\WooCommerce\Blueprint\Steps\Step;
/**
* Class SetWCPaymentGateways
*
* This class sets WooCommerce payment gateways and extends the Step class.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Steps
*/
class SetWCPaymentGateways extends Step {
/**
* Payment gateways.
*
* @var array $payment_gateways Array of payment gateways.
*/
protected array $payment_gateways = array();
/**
* Constructor.
*
* @param array $payment_gateways Optional array of payment gateways.
*/
public function __construct( array $payment_gateways = array() ) {
$this->payment_gateways = $payment_gateways;
}
/**
* Add a payment gateway.
*
* @param string $id The ID of the payment gateway.
* @param string $title The title of the payment gateway.
* @param string $description The description of the payment gateway.
* @param string $enabled Whether the payment gateway is enabled ('yes' or 'no').
*/
public function add_payment_gateway( $id, $title, $description, $enabled ) {
$this->payment_gateways[ $id ] = array(
'title' => $title,
'description' => $description,
'enabled' => $enabled,
);
}
/**
* Get the name of the step.
*
* @return string
*/
public static function get_step_name(): string {
return 'setWCPaymentGateways';
}
/**
* Get the schema for the step.
*
* @param int $version Optional version number of the schema.
* @return array The schema array.
*/
public static function get_schema( $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( 'setWCPaymentGateways' ),
),
'payment_gateways' => array(
'type' => 'object',
'patternProperties' => array(
'^[a-zA-Z0-9_]+$' => array(
'type' => 'object',
'properties' => array(
'title' => array(
'type' => 'string',
),
'description' => array(
'type' => 'string',
),
'enabled' => array(
'type' => 'string',
'enum' => array( 'yes', 'no' ),
),
),
'required' => array( 'title', 'description', 'enabled' ),
),
),
'additionalProperties' => false,
),
),
'required' => array( 'step', 'payment_gateways' ),
);
}
/**
* Prepare the JSON array for the step.
*
* @return array The JSON array.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'payment_gateways' => $this->payment_gateways,
);
}
}

View File

@@ -0,0 +1,232 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Steps;
use Automattic\WooCommerce\Blueprint\Steps\Step;
/**
* Class SetWCShipping
*
* This class sets WooCommerce shipping settings and extends the Step class.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Steps
*/
class SetWCShipping extends Step {
/**
* Shipping methods.
*
* @var array $methods Shipping methods.
*/
private array $methods;
/**
* Shipping locations.
*
* @var array $locations Shipping locations.
*/
private array $locations;
/**
* Shipping zones.
*
* @var array $zones Shipping zones.
*/
private array $zones;
/**
* Shipping terms.
*
* @var array $terms Shipping terms.
*/
private array $terms;
/**
* Shipping classes.
*
* @var array $classes Shipping classes.
*/
private array $classes;
/**
* Local pickup settings.
*
* @var array $local_pickup Local pickup settings.
*/
private array $local_pickup;
/**
* Constructor.
*
* @param array $methods Shipping methods.
* @param array $locations Shipping locations.
* @param array $zones Shipping zones.
* @param array $terms Shipping terms.
* @param array $classes Shipping classes.
* @param array $local_pickup Local pickup settings.
*/
public function __construct( array $methods, array $locations, array $zones, array $terms, array $classes, array $local_pickup ) {
$this->methods = $methods;
$this->locations = $locations;
$this->zones = $zones;
$this->terms = $terms;
$this->classes = $classes;
$this->local_pickup = $local_pickup;
}
/**
* Prepare the JSON array for the step.
*
* @return array The JSON array.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'values' => array(
'shipping_methods' => $this->methods,
'shipping_locations' => $this->locations,
'shipping_zones' => $this->zones,
'terms' => $this->terms,
'classes' => $this->classes,
'local_pickup' => $this->local_pickup,
),
);
}
/**
* Get the name of the step.
*
* @return string
*/
public static function get_step_name(): string {
return 'setWCShipping';
}
/**
* Get the schema for the step.
*
* @param int $version Optional version number of the schema.
* @return array The schema array.
*/
public static function get_schema( $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'values' => array(
'type' => 'object',
'properties' => array(
'classes' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'term_taxonomy_id' => array( 'type' => 'string' ),
'term_id' => array( 'type' => 'string' ),
'taxonomy' => array( 'type' => 'string' ),
'description' => array( 'type' => 'string' ),
'parent' => array( 'type' => 'string' ),
'count' => array( 'type' => 'string' ),
),
'required' => array( 'term_taxonomy_id', 'term_id', 'taxonomy', 'description', 'parent', 'count' ),
),
),
'terms' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'term_id' => array( 'type' => 'string' ),
'name' => array( 'type' => 'string' ),
'slug' => array( 'type' => 'string' ),
'term_group' => array( 'type' => 'string' ),
),
'required' => array( 'term_id', 'name', 'slug', 'term_group' ),
),
),
'local_pickup' => array(
'type' => 'object',
'properties' => array(
'general' => array(
'type' => 'object',
'properties' => array(
'enabled' => array( 'type' => 'string' ),
'title' => array( 'type' => 'string' ),
'tax_status' => array( 'type' => 'string' ),
'cost' => array( 'type' => 'string' ),
),
),
'locations' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'name' => array( 'type' => 'string' ),
'address' => array(
'type' => 'object',
'properties' => array(
'address_1' => array( 'type' => 'string' ),
'city' => array( 'type' => 'string' ),
'state' => array( 'type' => 'string' ),
'postcode' => array( 'type' => 'string' ),
'country' => array( 'type' => 'string' ),
),
),
'details' => array( 'type' => 'string' ),
'enabled' => array( 'type' => 'boolean' ),
),
),
),
),
),
'shipping_methods' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'zone_id' => array( 'type' => 'string' ),
'instance_id' => array( 'type' => 'string' ),
'method_id' => array( 'type' => 'string' ),
'method_order' => array( 'type' => 'string' ),
'is_enabled' => array( 'type' => 'string' ),
),
'required' => array( 'zone_id', 'instance_id', 'method_id', 'method_order', 'is_enabled' ),
),
),
'shipping_locations' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'location_id' => array( 'type' => 'string' ),
'zone_id' => array( 'type' => 'string' ),
'location_code' => array( 'type' => 'string' ),
'location_type' => array( 'type' => 'string' ),
),
'required' => array( 'location_id', 'zone_id', 'location_code', 'location_type' ),
),
),
'shipping_zones' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'zone_id' => array( 'type' => 'string' ),
'zone_name' => array( 'type' => 'string' ),
'zone_order' => array( 'type' => 'string' ),
),
'required' => array( 'zone_id', 'zone_name', 'zone_order' ),
),
),
),
),
),
'required' => array( 'step', 'values' ),
);
}
}

View File

@@ -0,0 +1,134 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Admin\Features\Blueprint\Steps;
use Automattic\WooCommerce\Blueprint\Steps\Step;
/**
* Class SetWCTaxRates
*
* This class sets WooCommerce tax rates and extends the Step class.
*
* @package Automattic\WooCommerce\Admin\Features\Blueprint\Steps
*/
class SetWCTaxRates extends Step {
/**
* Tax rates.
*
* @var array $rates Tax rates.
*/
private array $rates;
/**
* Tax rate locations.
*
* @var array $locations Tax rate locations.
*/
private array $locations;
/**
* Constructor.
*
* @param array $rates Tax rates.
* @param array $locations Tax rate locations.
*/
public function __construct( array $rates, array $locations ) {
$this->rates = $rates;
$this->locations = $locations;
}
/**
* Prepare the JSON array for the step.
*
* @return array The JSON array.
*/
public function prepare_json_array(): array {
return array(
'step' => static::get_step_name(),
'values' => array(
'rates' => $this->rates,
'locations' => $this->locations,
),
);
}
/**
* Get the name of the step.
*
* @return string
*/
public static function get_step_name(): string {
return 'setWCTaxRates';
}
/**
* Get the schema for the step.
*
* @param int $version Optional version number of the schema.
* @return array The schema array.
*/
public static function get_schema( $version = 1 ): array {
return array(
'type' => 'object',
'properties' => array(
'step' => array(
'type' => 'string',
'enum' => array( static::get_step_name() ),
),
'values' => array(
'type' => 'object',
'properties' => array(
'rates' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'tax_rate_id' => array( 'type' => 'string' ),
'tax_rate_country' => array( 'type' => 'string' ),
'tax_rate_state' => array( 'type' => 'string' ),
'tax_rate' => array( 'type' => 'string' ),
'tax_rate_name' => array( 'type' => 'string' ),
'tax_rate_priority' => array( 'type' => 'string' ),
'tax_rate_compound' => array( 'type' => 'string' ),
'tax_rate_shipping' => array( 'type' => 'string' ),
'tax_rate_order' => array( 'type' => 'string' ),
'tax_rate_class' => array( 'type' => 'string' ),
),
'required' => array(
'tax_rate_id',
'tax_rate_country',
'tax_rate_state',
'tax_rate',
'tax_rate_name',
'tax_rate_priority',
'tax_rate_compound',
'tax_rate_shipping',
'tax_rate_order',
'tax_rate_class',
),
),
),
'locations' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'location_id' => array( 'type' => 'string' ),
'location_code' => array( 'type' => 'string' ),
'tax_rate_id' => array( 'type' => 'string' ),
'location_type' => array( 'type' => 'string' ),
),
'required' => array( 'location_id', 'location_code', 'tax_rate_id', 'location_type' ),
),
),
),
'required' => array( 'rates' ),
),
),
'required' => array( 'step', 'values' ),
);
}
}

View File

@@ -5,12 +5,13 @@ namespace Automattic\WooCommerce\Admin\Features;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Admin\WCAdminHelper;
use Automattic\WooCommerce\Internal\Admin\WCAdminUser;
/**
* Takes care of Launch Your Store related actions.
*/
class LaunchYourStore {
const BANNER_DISMISS_USER_META_KEY = 'woocommerce_coming_soon_banner_dismissed';
const BANNER_DISMISS_USER_META_KEY = 'coming_soon_banner_dismissed';
/**
* Constructor.
*/
@@ -21,6 +22,7 @@ class LaunchYourStore {
add_action( 'init', array( $this, 'register_launch_your_store_user_meta_fields' ) );
add_filter( 'woocommerce_tracks_event_properties', array( $this, 'append_coming_soon_global_tracks' ), 10, 2 );
add_action( 'wp_login', array( $this, 'reset_woocommerce_coming_soon_banner_dismissed' ), 10, 2 );
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
}
/**
@@ -160,7 +162,10 @@ class LaunchYourStore {
return false;
}
if ( get_user_meta( $current_user_id, self::BANNER_DISMISS_USER_META_KEY, true ) === 'yes' ) {
$has_dismissed_banner = WCAdminUser::get_user_data_field( $current_user_id, self::BANNER_DISMISS_USER_META_KEY )
// Remove this check in WC 9.4.
|| get_user_meta( $current_user_id, 'woocommerce_' . self::BANNER_DISMISS_USER_META_KEY, true ) === 'yes';
if ( $has_dismissed_banner ) {
return false;
}
@@ -198,6 +203,8 @@ class LaunchYourStore {
/**
* Register user meta fields for Launch Your Store.
*
* This should be removed in WC 9.4.
*/
public function register_launch_your_store_user_meta_fields() {
if ( ! $this->is_manager_or_admin() ) {
@@ -217,7 +224,7 @@ class LaunchYourStore {
register_meta(
'user',
self::BANNER_DISMISS_USER_META_KEY,
'woocommerce_coming_soon_banner_dismissed',
array(
'type' => 'string',
'description' => 'Indicate whether the user has dismissed the coming soon notice or not.',
@@ -227,6 +234,22 @@ class LaunchYourStore {
);
}
/**
* Register user meta fields for Launch Your Store.
*
* @param array $user_data_fields user data fields.
* @return array
*/
public function add_user_data_fields( $user_data_fields ) {
return array_merge(
$user_data_fields,
array(
'launch_your_store_tour_hidden',
self::BANNER_DISMISS_USER_META_KEY,
)
);
}
/**
* Reset 'woocommerce_coming_soon_banner_dismissed' user meta to 'no'.
*
@@ -236,9 +259,9 @@ class LaunchYourStore {
* @param object $user user object.
*/
public function reset_woocommerce_coming_soon_banner_dismissed( $user_login, $user ) {
$existing_meta = get_user_meta( $user->ID, self::BANNER_DISMISS_USER_META_KEY, true );
$existing_meta = WCAdminUser::get_user_data_field( $user->ID, self::BANNER_DISMISS_USER_META_KEY );
if ( 'yes' === $existing_meta ) {
update_user_meta( $user->ID, self::BANNER_DISMISS_USER_META_KEY, 'no' );
WCAdminUser::update_user_data_field( $user->ID, self::BANNER_DISMISS_USER_META_KEY, 'no' );
}
}
}

View File

@@ -2,6 +2,7 @@
/**
* WooCommerce Navigation Core Menu
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Admin
*/

View File

@@ -2,6 +2,7 @@
/**
* WooCommerce Navigation Favorite
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Navigation
*/

View File

@@ -2,17 +2,17 @@
/**
* Navigation Experience
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Admin
*/
namespace Automattic\WooCommerce\Admin\Features\Navigation;
use Automattic\WooCommerce\Internal\Admin\Survey;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use WC_Tracks;
/**
* Contains logic for the Navigation
@@ -23,115 +23,27 @@ class Init {
*/
const TOGGLE_OPTION_NAME = 'woocommerce_navigation_enabled';
/**
* Determines if the feature has been toggled on or off.
*
* @var boolean
*/
protected static $is_updated = false;
/**
* Hook into WooCommerce.
*/
public function __construct() {
add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_opt_out_scripts' ) );
if ( Features::is_enabled( 'navigation' ) ) {
Menu::instance()->init();
CoreMenu::instance()->init();
Screen::instance()->init();
// Disable the option to turn off the feature.
update_option( self::TOGGLE_OPTION_NAME, 'no' );
if ( class_exists( 'WC_Tracks' ) ) {
WC_Tracks::record_event( 'deprecated_navigation_in_use' );
}
}
}
/**
* Add the feature toggle to the features settings.
* Create a deprecation notice.
*
* @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
*
* @param array $features Feature sections.
* @return array
* @param string $fcn The function that is deprecated.
*/
public static function add_feature_toggle( $features ) {
return $features;
}
/**
* Determine if sufficient versions are present to support Navigation feature
*/
public function is_nav_compatible() {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
$gutenberg_minimum_version = '9.0.0'; // https://github.com/WordPress/gutenberg/releases/tag/v9.0.0.
$wp_minimum_version = '5.6';
$has_gutenberg = is_plugin_active( 'gutenberg/gutenberg.php' );
$gutenberg_version = $has_gutenberg ? get_plugin_data( WP_PLUGIN_DIR . '/gutenberg/gutenberg.php' )['Version'] : false;
if ( $gutenberg_version && version_compare( $gutenberg_version, $gutenberg_minimum_version, '>=' ) ) {
return true;
}
// Get unmodified $wp_version.
include ABSPATH . WPINC . '/version.php';
// Strip '-src' from the version string. Messes up version_compare().
$wp_version = str_replace( '-src', '', $wp_version );
if ( version_compare( $wp_version, $wp_minimum_version, '>=' ) ) {
return true;
}
return false;
}
/**
* Reloads the page when the option is toggled to make sure all nav features are loaded.
*
* @param string $old_value Old value.
* @param string $value New value.
*/
public static function reload_page_on_toggle( $old_value, $value ) {
if ( $old_value === $value ) {
return;
}
if ( 'yes' !== $value ) {
update_option( 'woocommerce_navigation_show_opt_out', 'yes' );
}
self::$is_updated = true;
}
/**
* Reload the page if the setting has been updated.
*/
public static function maybe_reload_page() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
return;
}
wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
exit();
}
/**
* Enqueue the opt out scripts.
*/
public function maybe_enqueue_opt_out_scripts() {
if ( get_option( 'woocommerce_navigation_show_opt_out', 'no' ) !== 'yes' ) {
return;
}
WCAdminAssets::register_style( 'navigation-opt-out', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'navigation-opt-out', true );
wp_localize_script(
'wc-admin-navigation-opt-out',
'surveyData',
array(
'url' => Survey::get_url( '/new-navigation-opt-out' ),
)
);
delete_option( 'woocommerce_navigation_show_opt_out' );
public static function deprecation_notice( $fcn ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Automattic\WooCommerce\Admin\Features\Navigation\\' . $fcn . ' is deprecated since 9.3 with no alternative. Navigation classes will be removed in WooCommerce 9.4' );
}
}

View File

@@ -2,6 +2,7 @@
/**
* WooCommerce Navigation Menu
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Navigation
*/
@@ -10,6 +11,7 @@ namespace Automattic\WooCommerce\Admin\Features\Navigation;
use Automattic\WooCommerce\Admin\Features\Navigation\Favorites;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
/**
* Contains logic for the WooCommerce Navigation menu.
@@ -95,172 +97,33 @@ class Menu {
/**
* Init.
*
* @internal
*/
public function init() {
add_action( 'admin_menu', array( $this, 'add_core_items' ), 100 );
add_filter( 'admin_enqueue_scripts', array( $this, 'enqueue_data' ), 20 );
add_filter( 'admin_menu', array( $this, 'migrate_core_child_items' ), PHP_INT_MAX - 1 );
add_filter( 'admin_menu', array( $this, 'migrate_menu_items' ), PHP_INT_MAX - 2 );
}
final public function init() {}
/**
* Convert a WordPress menu callback to a URL.
*
* @param string $callback Menu callback.
* @return string
*/
public static function get_callback_url( $callback ) {
// Return the full URL.
if ( strpos( $callback, 'http' ) === 0 ) {
return $callback;
}
$pos = strpos( $callback, '?' );
$file = $pos > 0 ? substr( $callback, 0, $pos ) : $callback;
if ( file_exists( ABSPATH . "/wp-admin/$file" ) ) {
return $callback;
}
return 'admin.php?page=' . $callback;
}
public static function get_callback_url() {}
/**
* Get the parent key if one exists.
*
* @param string $callback Callback or URL.
* @return string|null
*/
public static function get_parent_key( $callback ) {
global $submenu;
if ( ! $submenu ) {
return null;
}
// This is already a parent item.
if ( isset( $submenu[ $callback ] ) ) {
return null;
}
foreach ( $submenu as $key => $menu ) {
foreach ( $menu as $item ) {
if ( $item[ self::CALLBACK ] === $callback ) {
return $key;
}
}
}
return null;
}
public static function get_parent_key() {}
/**
* Adds a top level menu item to the navigation.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'url' => (string) URL or callback to be used. Required.
* 'order' => (int) Menu item order.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'menuId' => (string) The ID of the menu to add the category to.
* ).
*/
private static function add_category( $args ) {
if ( ! isset( $args['id'] ) || isset( self::$menu_items[ $args['id'] ] ) ) {
return;
}
$defaults = array(
'id' => '',
'title' => '',
'order' => 100,
'migrate' => true,
'menuId' => 'primary',
'isCategory' => true,
);
$menu_item = wp_parse_args( $args, $defaults );
$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
unset( $menu_item['url'] );
unset( $menu_item['capability'] );
if ( ! isset( $menu_item['parent'] ) ) {
$menu_item['parent'] = 'woocommerce';
$menu_item['backButtonLabel'] = __(
'WooCommerce Home',
'woocommerce'
);
}
self::$menu_items[ $menu_item['id'] ] = $menu_item;
self::$categories[ $menu_item['id'] ] = array();
self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];
if ( isset( $args['url'] ) ) {
self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
}
private static function add_category() {
Init::deprecation_notice( 'Menu::add_category' );
}
/**
* Adds a child menu item to the navigation.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'parent' => (string) Parent menu item ID.
* 'capability' => (string) Capability to view this menu item.
* 'url' => (string) URL or callback to be used. Required.
* 'order' => (int) Menu item order.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'menuId' => (string) The ID of the menu to add the item to.
* 'matchExpression' => (string) A regular expression used to identify if the menu item is active.
* ).
*/
private static function add_item( $args ) {
if ( ! isset( $args['id'] ) ) {
return;
}
if ( isset( self::$menu_items[ $args['id'] ] ) ) {
wc_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: Duplicate menu item path. */
esc_html__( 'You have attempted to register a duplicate item with WooCommerce Navigation: %1$s', 'woocommerce' ),
'`' . $args['id'] . '`'
),
'6.5.0'
);
return;
}
$defaults = array(
'id' => '',
'title' => '',
'capability' => 'manage_woocommerce',
'url' => '',
'order' => 100,
'migrate' => true,
'menuId' => 'primary',
);
$menu_item = wp_parse_args( $args, $defaults );
$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
$menu_item['url'] = self::get_callback_url( $menu_item['url'] );
if ( ! isset( $menu_item['parent'] ) ) {
$menu_item['parent'] = 'woocommerce';
}
$menu_item['menuId'] = self::get_item_menu_id( $menu_item );
self::$menu_items[ $menu_item['id'] ] = $menu_item;
self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];
if ( isset( $args['url'] ) ) {
self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
}
private static function add_item() {
Init::deprecation_notice( 'Menu::add_item' );
}
/**
@@ -287,112 +150,25 @@ class Menu {
/**
* Adds a plugin category.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'url' => (string) URL or callback to be used. Required.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'order' => (int) Menu item order.
* ).
*/
public static function add_plugin_category( $args ) {
$category_args = array_merge(
$args,
array(
'menuId' => 'plugins',
)
);
if ( ! isset( $category_args['parent'] ) ) {
unset( $category_args['order'] );
}
$menu_id = self::get_item_menu_id( $category_args );
if ( ! in_array( $menu_id, array( 'plugins', 'favorites' ), true ) ) {
return;
}
$category_args['menuId'] = $menu_id;
self::add_category( $category_args );
public static function add_plugin_category() {
Init::deprecation_notice( 'Menu::add_plugin_category' );
}
/**
* Adds a plugin item.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'parent' => (string) Parent menu item ID.
* 'capability' => (string) Capability to view this menu item.
* 'url' => (string) URL or callback to be used. Required.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* 'order' => (int) Menu item order.
* 'matchExpression' => (string) A regular expression used to identify if the menu item is active.
* ).
*/
public static function add_plugin_item( $args ) {
if ( ! isset( $args['parent'] ) ) {
unset( $args['order'] );
}
$item_args = array_merge(
$args,
array(
'menuId' => 'plugins',
)
);
$menu_id = self::get_item_menu_id( $item_args );
if ( 'plugins' !== $menu_id ) {
return;
}
self::add_item( $item_args );
public static function add_plugin_item() {
Init::deprecation_notice( 'Menu::add_plugin_item' );
}
/**
* Adds a plugin setting item.
*
* @param array $args Array containing the necessary arguments.
* $args = array(
* 'id' => (string) The unique ID of the menu item. Required.
* 'title' => (string) Title of the menu item. Required.
* 'capability' => (string) Capability to view this menu item.
* 'url' => (string) URL or callback to be used. Required.
* 'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
* ).
*/
public static function add_setting_item( $args ) {
unset( $args['order'] );
if ( isset( $args['parent'] ) || isset( $args['menuId'] ) ) {
error_log( // phpcs:ignore
sprintf(
/* translators: 1: Duplicate menu item path. */
esc_html__( 'The item ID %1$s attempted to register using an invalid option. The arguments `menuId` and `parent` are not allowed for add_setting_item()', 'woocommerce' ),
'`' . $args['id'] . '`'
)
);
}
$item_args = array_merge(
$args,
array(
'menuId' => 'secondary',
'parent' => 'woocommerce-settings',
)
);
self::add_item( $item_args );
public static function add_setting_item() {
Init::deprecation_notice( 'Menu::add_setting_item' );
}
/**
* Get menu item templates for a given post type.
*

View File

@@ -2,12 +2,14 @@
/**
* WooCommerce Navigation Screen
*
* @deprecated 9.3.0 Navigation is no longer a feature and its classes will be removed in WooCommerce 9.4.
* @package Woocommerce Navigation
*/
namespace Automattic\WooCommerce\Admin\Features\Navigation;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
/**
* Contains logic for the WooCommerce Navigation menu.
@@ -85,6 +87,8 @@ class Screen {
* @return bool
*/
public static function is_woocommerce_page() {
Init::deprecation_notice( 'Screen::is_woocommerce_page' );
global $pagenow;
// Get taxonomy if on a taxonomy screen.
@@ -218,23 +222,15 @@ class Screen {
/**
* Register post type for use in WooCommerce Navigation screens.
*
* @param string $post_type Post type to add.
*/
public static function register_post_type( $post_type ) {
if ( ! in_array( $post_type, self::$post_types, true ) ) {
self::$post_types[] = $post_type;
}
public static function register_post_type() {
Init::deprecation_notice( 'Screen::register_post_type' );
}
/**
* Register taxonomy for use in WooCommerce Navigation screens.
*
* @param string $taxonomy Taxonomy to add.
*/
public static function register_taxonomy( $taxonomy ) {
if ( ! in_array( $taxonomy, self::$taxonomies, true ) ) {
self::$taxonomies[] = $taxonomy;
}
public static function register_taxonomy() {
Init::deprecation_notice( 'Screen::register_taxonomy' );
}
}

View File

@@ -117,7 +117,6 @@ class TaskLists {
'Payments',
'Tax',
'Shipping',
'Marketing',
'LaunchYourStore',
);
@@ -165,6 +164,7 @@ class TaskLists {
),
),
'tasks' => array(
'Marketing',
'ExtendStore',
'AdditionalPayments',
'GetMobileApp',
@@ -297,7 +297,6 @@ class TaskLists {
$task_list->add_task( $task );
}
}
}
/**
@@ -318,8 +317,8 @@ class TaskLists {
public static function get_lists_by_ids( $ids ) {
return array_filter(
self::$lists,
function( $list ) use ( $ids ) {
return in_array( $list->get_list_id(), $ids, true );
function ( $task_list ) use ( $ids ) {
return in_array( $task_list->get_list_id(), $ids, true );
}
);
}
@@ -404,25 +403,31 @@ class TaskLists {
/**
* Return number of setup tasks remaining
*
* @return number
* This is not updated immediately when a task is completed, but rather when task is marked as complete in the database to reduce performance impact.
*
* @return int|null
*/
public static function setup_tasks_remaining() {
$setup_list = self::get_list( 'setup' );
if ( ! $setup_list || $setup_list->is_hidden() || $setup_list->has_previously_completed() || $setup_list->is_complete() ) {
if ( ! $setup_list || $setup_list->is_hidden() || $setup_list->has_previously_completed() ) {
return;
}
$remaining_tasks = array_values(
$viewable_tasks = $setup_list->get_viewable_tasks();
$completed_tasks = get_option( Task::COMPLETED_OPTION, array() );
if ( ! is_array( $completed_tasks ) ) {
$completed_tasks = array();
}
return count(
array_filter(
$setup_list->get_viewable_tasks(),
function( $task ) {
return ! $task->is_complete();
$viewable_tasks,
function ( $task ) use ( $completed_tasks ) {
return ! in_array( $task->get_id(), $completed_tasks, true );
}
)
);
return count( $remaining_tasks );
}
/**
@@ -443,7 +448,6 @@ class TaskLists {
break;
}
}
}
/**

View File

@@ -188,7 +188,7 @@ class AdditionalPayments extends Payments {
*/
private static function get_suggestion_gateways( $filter_by = 'category_additional' ) {
$country = wc_get_base_location()['country'];
$plugin_suggestions = Init::get_suggestions();
$plugin_suggestions = Init::get_cached_or_default_suggestions();
$plugin_suggestions = array_filter(
$plugin_suggestions,
function( $plugin ) use ( $country, $filter_by ) {

View File

@@ -56,79 +56,40 @@ class Marketing extends Task {
return __( '2 minutes', 'woocommerce' );
}
/**
* Task completion.
*
* @return bool
*/
public function is_complete() {
if ( null === $this->is_complete_result ) {
$this->is_complete_result = self::has_installed_extensions();
}
return $this->is_complete_result;
}
/**
* Task visibility.
*
* @return bool
*/
public function can_view() {
return Features::is_enabled( 'remote-free-extensions' ) && count( self::get_plugins() ) > 0;
return Features::is_enabled( 'remote-free-extensions' );
}
/**
* Get the marketing plugins.
*
* @deprecated 9.3.0 Removed to improve performance.
* @return array
*/
public static function get_plugins() {
$bundles = RemoteFreeExtensions::get_extensions(
array(
'task-list/reach',
'task-list/grow',
)
);
return array_reduce(
$bundles,
function( $plugins, $bundle ) {
$visible = array();
foreach ( $bundle['plugins'] as $plugin ) {
if ( $plugin->is_visible ) {
$visible[] = $plugin;
}
}
return array_merge( $plugins, $visible );
},
array()
wc_deprecated_function(
__METHOD__,
'9.3.0'
);
return array();
}
/**
* Check if the store has installed marketing extensions.
*
* @deprecated 9.3.0 Removed to improve performance.
* @return bool
*/
public static function has_installed_extensions() {
$plugins = self::get_plugins();
$remaining = array();
$installed = array();
foreach ( $plugins as $plugin ) {
if ( ! $plugin->is_installed ) {
$remaining[] = $plugin;
} else {
$installed[] = $plugin;
}
}
// Make sure the task has been actioned and a marketing extension has been installed.
if ( count( $installed ) > 0 && Task::is_task_actioned( 'marketing' ) ) {
return true;
}
wc_deprecated_function(
__METHOD__,
'9.3.0'
);
return false;
}
}

View File

@@ -7,6 +7,7 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init as Suggestions;
use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
/**
* WooCommercePayments Task
@@ -179,11 +180,11 @@ class WooCommercePayments extends Task {
* @return bool
*/
public static function is_supported() {
$suggestions = Suggestions::get_suggestions();
$suggestions = Suggestions::get_suggestions( DefaultPaymentGateways::get_all() );
$suggestion_plugins = array_merge(
...array_filter(
array_column( $suggestions, 'plugins' ),
function( $plugins ) {
function ( $plugins ) {
return is_array( $plugins );
}
)

View File

@@ -7,8 +7,6 @@ namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaysController;
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
/**
@@ -63,6 +61,31 @@ class Init extends RemoteSpecsEngine {
return $specs_to_return;
}
/**
* Gets either cached or default suggestions.
*
* @return array
*/
public static function get_cached_or_default_suggestions() {
$specs = 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' )
? DefaultPaymentGateways::get_all()
: PaymentGatewaySuggestionsDataSourcePoller::get_instance()->get_cached_specs();
if ( ! is_array( $specs ) || 0 === count( $specs ) ) {
$specs = DefaultPaymentGateways::get_all();
}
/**
* Allows filtering of payment gateway suggestion specs
*
* @since 6.4.0
*
* @param array Gateway specs.
*/
$specs = apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', $specs );
$results = EvaluateSuggestion::evaluate_specs( $specs );
return $results['suggestions'];
}
/**
* Delete the specs transient.
*/

View File

@@ -2,7 +2,7 @@
namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;
use Automattic\WooCommerce\Admin\DataSourcePoller;
use Automattic\WooCommerce\Admin\RemoteSpecs\DataSourcePoller;
/**
* Specs data source poller class for payment gateway suggestions.

View File

@@ -574,6 +574,6 @@ class PageController {
* TODO: See usage in `admin.php`. This needs refactored and implemented properly in core.
*/
public static function is_embed_page() {
return wc_admin_is_connected_page() || ( ! self::is_admin_page() && class_exists( 'Automattic\WooCommerce\Admin\Features\Navigation\Screen' ) && Screen::is_woocommerce_page() );
return wc_admin_is_connected_page();
}
}

View File

@@ -14,6 +14,7 @@ use Automatic_Upgrader_Skin;
use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsyncPluginsInstallLogger;
use Automattic\WooCommerce\Admin\PluginsInstallLoggers\PluginsInstallLogger;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use Automattic\WooCommerce\Utilities\PluginUtil;
use Plugin_Upgrader;
use WC_Helper;
use WC_Helper_Updater;
@@ -80,7 +81,7 @@ class PluginsHelper {
*
* @param string $slug Plugin slug to get path for.
*
* @return string|false
* @return string|false The plugin path or false if the plugin is not installed.
*/
public static function get_plugin_path_from_slug( $slug ) {
$plugins = get_plugins();
@@ -137,16 +138,25 @@ class PluginsHelper {
/**
* Get an array of active plugin slugs.
*
* @return array
* The list will include both network active and site active plugins.
*
* @return array The list of active plugin slugs.
*/
public static function get_active_plugin_slugs() {
return array_map(
function ( $plugin_path ) {
$path_parts = explode( '/', $plugin_path );
return array_unique(
array_map(
function ( $absolute_path ) {
// Make the path relative to the plugins directory.
$plugin_path = str_replace( WP_PLUGIN_DIR . '/', '', $absolute_path );
return $path_parts[0];
},
get_option( 'active_plugins', array() )
// Split the path to get the plugin slug (aka the directory name).
$path_parts = explode( '/', $plugin_path );
return $path_parts[0];
},
// Use this method as it is the most bulletproof way to get the active plugins.
wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins()
)
);
}
@@ -173,7 +183,7 @@ class PluginsHelper {
public static function is_plugin_active( $plugin ) {
$plugin_path = self::get_plugin_path_from_slug( $plugin );
return $plugin_path ? in_array( $plugin_path, get_option( 'active_plugins', array() ), true ) : false;
return $plugin_path && \is_plugin_active( $plugin_path );
}
/**
@@ -829,7 +839,7 @@ class PluginsHelper {
$subscriptions,
function ( $sub ) {
return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
&& $sub['active']
&& ( $sub['active'] || empty( $sub['connections'] ) ) // Active on current site or not connected to any sites.
&& $sub['expiring']
&& ! $sub['autorenew'];
},
@@ -907,7 +917,7 @@ class PluginsHelper {
$subscriptions,
function ( $sub ) {
return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
&& $sub['active']
&& ( $sub['active'] || empty( $sub['connections'] ) ) // Active on current site or not connected to any sites.
&& $sub['expired']
&& ! $sub['lifetime'];
},

View File

@@ -106,9 +106,9 @@ abstract class DataSourcePoller {
public function get_specs_from_data_sources() {
$locale = get_user_locale();
$specs_group = get_transient( $this->args['transient_name'] ) ?? array();
$specs = isset( $specs_group[ $locale ] ) ? $specs_group[ $locale ] : array();
$specs = isset( $specs_group[ $locale ] ) ? $specs_group[ $locale ] : null;
if ( ! is_array( $specs ) || empty( $specs ) ) {
if ( ! is_array( $specs ) ) {
$this->read_specs_from_data_sources();
$specs_group = get_transient( $this->args['transient_name'] );
$specs = isset( $specs_group[ $locale ] ) ? $specs_group[ $locale ] : array();
@@ -126,6 +126,29 @@ abstract class DataSourcePoller {
return false !== $specs ? $specs : array();
}
/**
* Gets specs from cache if it exists.
*
* @return array list of specs.
*/
public function get_cached_specs() {
$locale = get_user_locale();
$specs_group = get_transient( $this->args['transient_name'] ) ?? array();
$specs = isset( $specs_group[ $locale ] ) ? $specs_group[ $locale ] : null;
/**
* Filter specs.
*
* @param array $specs List of specs.
* @param string $this->id Spec identifier.
*
* @since 8.8.0
*/
$specs = apply_filters( self::FILTER_NAME_SPECS, $specs, $this->id );
return false !== $specs ? $specs : array();
}
/**
* Reads the data sources for specs and persists those specs.
*

Some files were not shown because too many files have changed in this diff Show More