mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* fix: address WordPress.org plugin review feedback - Prefix the session transaction queue transient with the plugin's graphql_woocommerce_ namespace instead of the generic "woo_" word, to avoid collisions (Plugin Directory: prefix data storage). - Declare the WooCommerce dependency via the "Requires Plugins: woocommerce" plugin header. - Bump README.txt "Tested up to" to 7.0. - Ship composer.json in the distributed plugin (drop it, and composer.lock, from composer archive excludes) so the build is reproducible/reviewable. * chore: rename plugin to "GraphQL for eCommerce" for trademark compliance The WordPress.org plugin review flagged the display name/slug for beginning with the "WPGraphQL" trademark (and the "WooGraphQL" portmanteau of the WooCommerce mark), which can imply official affiliation. - Display name (plugin header + readme title) -> "GraphQL for eCommerce". - Slug/text domain -> "graphql-for-ecommerce" (header, all i18n string literals, and the PHPCS WordPress.WP.I18n text_domain config). - Update user-facing notices/errors that named the old plugin. "WooGraphQL" remains the project's informal nickname (repo, docs, community), just not in the WordPress.org directory's official name/slug. Internal file names and GitHub URLs are unchanged. * fix: keep test-only dev deps out of the committed composer.json The committed manifest mirrors develop (lint/stan dev deps only); CI adds the test suite deps at runtime via `composer installTestEnv`. A previous commit captured the installTestEnv-modified composer.json, desyncing it from composer.lock and breaking `composer install` in CI. * chore: regenerate composer.lock (refresh dev dependencies) Regenerate the lock from the manifest so it is in sync (fixes the CI `composer install` failure) and refresh dependencies in the process — firebase/php-jwt v7.0.4 -> v7.1.0 plus 11 others, with vendor-prefixed re-strauss'd to match. Full wpunit suite passes against the updated deps (305 tests, 835 assertions). * chore: rename text domain in createdVia/attribution strings from #1018 #1018 (createdVia + order attribution) merged into develop after the rename commit was authored, so its new i18n strings still used the old 'wp-graphql-woocommerce' text domain. Update them to 'graphql-for-ecommerce' to match the rename.
372 lines
11 KiB
PHP
372 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* ConnectionResolver - Order_Connection_Resolver
|
|
*
|
|
* Resolves connections to Orders
|
|
*
|
|
* @package WPGraphQL\WooCommerce\Data\Connection
|
|
* @since 0.0.1
|
|
*/
|
|
|
|
namespace WPGraphQL\WooCommerce\Data\Connection;
|
|
|
|
use GraphQL\Error\InvariantViolation;
|
|
use WPGraphQL\Data\Connection\AbstractConnectionResolver;
|
|
|
|
/**
|
|
* Class Order_Connection_Resolver
|
|
*/
|
|
class Order_Connection_Resolver extends AbstractConnectionResolver {
|
|
/**
|
|
* Include CPT Loader connection common functions.
|
|
*/
|
|
use WC_CPT_Loader_Common;
|
|
|
|
/**
|
|
* The name of the post type, or array of post types the connection resolver is resolving for
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $post_type;
|
|
|
|
/**
|
|
* This stores the should
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $should_execute = false;
|
|
|
|
/**
|
|
* Refund_Connection_Resolver constructor.
|
|
*
|
|
* @param mixed $source The object passed down from the previous level in the Resolve tree.
|
|
* @param array $args The input arguments for the query.
|
|
* @param \WPGraphQL\AppContext $context The context of the request.
|
|
* @param \GraphQL\Type\Definition\ResolveInfo $info The resolve info passed down the Resolve tree.
|
|
* @param string $post_type The post type for the connection resolver.
|
|
*/
|
|
public function __construct( $source, $args, $context, $info, $post_type = 'shop_order' ) {
|
|
/**
|
|
* Set the post type for the resolver.
|
|
*/
|
|
$this->post_type = $post_type;
|
|
|
|
/**
|
|
* Call the parent construct to setup class data.
|
|
*/
|
|
parent::__construct( $source, $args, $context, $info );
|
|
|
|
/**
|
|
* Default to true.
|
|
*/
|
|
$this->should_execute = true;
|
|
}
|
|
|
|
/**
|
|
* Return the name of the loader to be used with the connection resolver
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_loader_name() {
|
|
return 'wc_post';
|
|
}
|
|
|
|
/**
|
|
* Given an ID, return the model for the entity or null
|
|
*
|
|
* @param integer $id Node ID.
|
|
*
|
|
* @return mixed|\WPGraphQL\WooCommerce\Model\Order|null
|
|
*/
|
|
public function get_node_by_id( $id ) {
|
|
return $this->get_cpt_model_by_id( $id );
|
|
}
|
|
|
|
/**
|
|
* Checks if user is authorized to query orders
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function should_execute() {
|
|
/**
|
|
* Get order post type.
|
|
*
|
|
* @var \WP_Post_Type $post_type_obj
|
|
*/
|
|
$post_type_obj = get_post_type_object( $this->post_type );
|
|
if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
|
|
return true;
|
|
} elseif ( isset( $this->query_args['customer_id'], $this->source ) && $this->source->ID === $this->query_args['customer_id'] ) {
|
|
return true;
|
|
} elseif ( isset( $this->query_args['billing_email'], $this->source ) && $this->source->email === $this->query_args['billing_email'] ) {
|
|
return true;
|
|
}
|
|
|
|
return $this->should_execute;
|
|
}
|
|
|
|
/**
|
|
* Sets whether or not the query should execute
|
|
*
|
|
* @param bool $should_execute Whether or not the query should execute.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function set_should_execute( bool $should_execute ) {
|
|
$this->should_execute = $should_execute;
|
|
}
|
|
|
|
/**
|
|
* Creates query arguments array
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_query_args() {
|
|
// Prepare for later use.
|
|
$last = ! empty( $this->args['last'] ) ? $this->args['last'] : null;
|
|
$first = ! empty( $this->args['first'] ) ? $this->args['first'] : null;
|
|
|
|
// Set the $query_args based on various defaults and primary input $args.
|
|
$query_args = [
|
|
'post_type' => $this->post_type,
|
|
'no_rows_found' => true,
|
|
'return' => 'ids',
|
|
'limit' => min( max( absint( $first ), absint( $last ), 10 ), $this->query_amount ) + 1,
|
|
];
|
|
|
|
/**
|
|
* Set posts_per_page the highest value of $first and $last, with a (filterable) max of 100
|
|
*/
|
|
$query_args['posts_per_page'] = $this->one_to_one ? 1 : min( max( absint( $first ), absint( $last ), 10 ), $this->query_amount ) + 1;
|
|
|
|
/**
|
|
* Set the graphql cursor args.
|
|
*/
|
|
$query_args['graphql_cursor_compare'] = ( ! empty( $last ) ) ? '>' : '<';
|
|
$query_args['graphql_after_cursor'] = $this->get_after_offset();
|
|
$query_args['graphql_before_cursor'] = $this->get_before_offset();
|
|
|
|
/**
|
|
* If the cursor offsets not empty,
|
|
* ignore sticky posts on the query
|
|
*/
|
|
if ( ! empty( $this->get_after_offset() ) || ! empty( $this->get_after_offset() ) ) {
|
|
$query_args['ignore_sticky_posts'] = true;
|
|
}
|
|
|
|
/**
|
|
* Pass the graphql $args to the WP_Query
|
|
*/
|
|
$query_args['graphql_args'] = $this->args;
|
|
|
|
/**
|
|
* Collect the input_fields and sanitize them to prepare them for sending to the WP_Query
|
|
*/
|
|
$input_fields = [];
|
|
if ( ! empty( $this->args['where'] ) ) {
|
|
$input_fields = $this->sanitize_input_fields( $this->args['where'] );
|
|
}
|
|
|
|
if ( ! empty( $input_fields ) ) {
|
|
$query_args = array_merge( $query_args, $input_fields );
|
|
}
|
|
|
|
/**
|
|
* If there's no orderby params in the inputArgs, set order based on the first/last argument
|
|
*/
|
|
if ( empty( $query_args['orderby'] ) ) {
|
|
$query_args['order'] = ! empty( $last ) ? 'ASC' : 'DESC';
|
|
$query_args['orderby'] = [ 'date' => $query_args['order'] ];
|
|
}
|
|
|
|
/**
|
|
* Filter the $query args to allow folks to customize queries programmatically
|
|
*
|
|
* @param array $query_args The args that will be passed to the WP_Query
|
|
* @param mixed $source The source that's passed down the GraphQL queries
|
|
* @param array<string, mixed>|null $args The inputArgs on the field
|
|
* @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree
|
|
* @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree
|
|
*/
|
|
$query_args = apply_filters( 'graphql_order_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info );
|
|
|
|
return $query_args;
|
|
}
|
|
|
|
/**
|
|
* Executes query
|
|
*
|
|
* @throws \GraphQL\Error\InvariantViolation Filter currently not supported for WC_Order_Query.
|
|
*
|
|
* @return \WC_Order_Query
|
|
*/
|
|
public function get_query() {
|
|
/** @var array<string, mixed> $query_args */
|
|
$query_args = $this->query_args;
|
|
$query = new \WC_Order_Query( $query_args );
|
|
|
|
if ( true === $query->get( 'suppress_filters', false ) ) {
|
|
throw new InvariantViolation( __( 'WC_Order_Query has been modified by a plugin or theme to suppress_filters, which will cause issues with WPGraphQL Execution. If you need to suppress filters for a specific reason within GraphQL, consider registering a custom field to the WPGraphQL Schema with a custom resolver.', 'graphql-for-ecommerce' ) );
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public function get_ids_from_query() {
|
|
$ids = ! empty( $this->query->get_orders() ) ? $this->query->get_orders() : [];
|
|
|
|
// If we're going backwards, we need to reverse the array.
|
|
if ( ! empty( $this->args['last'] ) ) {
|
|
$ids = array_reverse( $ids );
|
|
}
|
|
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* Returns meta keys to be used for connection ordering.
|
|
*
|
|
* @param bool $is_numeric Return numeric meta keys. Defaults to "true".
|
|
*
|
|
* @return array
|
|
*/
|
|
public function ordering_meta( $is_numeric = true ) {
|
|
if ( ! $is_numeric ) {
|
|
return apply_filters(
|
|
'woographql_order_connection_orderby_meta_keys',
|
|
[
|
|
'_order_key',
|
|
'_date_paid',
|
|
'_date_completed',
|
|
]
|
|
);
|
|
}
|
|
|
|
return apply_filters(
|
|
'woographql_order_connection_orderby_numeric_meta_keys',
|
|
[
|
|
'_cart_discount',
|
|
'_order_total',
|
|
'_order_tax',
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This sets up the "allowed" args, and translates the GraphQL-friendly keys to WP_Query
|
|
* friendly keys. There's probably a cleaner/more dynamic way to approach this, but
|
|
* this was quick. I'd be down to explore more dynamic ways to map this, but for
|
|
* now this gets the job done.
|
|
*
|
|
* @param array $where_args - arguments being used to filter query.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function sanitize_input_fields( array $where_args ) {
|
|
global $wpdb;
|
|
$args = $this->sanitize_common_inputs( $where_args );
|
|
|
|
$key_mapping = [
|
|
'post_parent' => 'parent',
|
|
'post_parent__not_in' => 'parent_exclude',
|
|
'post__not_in' => 'exclude',
|
|
];
|
|
|
|
foreach ( $key_mapping as $key => $field ) {
|
|
if ( isset( $args[ $key ] ) ) {
|
|
$args[ $field ] = $args[ $key ];
|
|
unset( $args[ $key ] );
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $where_args['statuses'] ) ) {
|
|
$single_status = 1 === count( $where_args['statuses'] );
|
|
$args['status'] = $single_status ? $where_args['statuses'][0] : $where_args['statuses'];
|
|
}
|
|
|
|
if ( ! empty( $where_args['customerId'] ) ) {
|
|
$args['customer_id'] = $where_args['customerId'];
|
|
}
|
|
|
|
if ( ! empty( $where_args['customersIn'] ) ) {
|
|
$args['customer'] = $where_args['customersIn'];
|
|
}
|
|
if ( ! empty( $where_args['billingEmail'] ) ) {
|
|
$billing_email = $where_args['billingEmail'];
|
|
$args['customer'] = ! empty( $args['customer'] )
|
|
? array_merge( $args['customer'], [ $billing_email ] )
|
|
: $billing_email;
|
|
}
|
|
|
|
// Search by product.
|
|
if ( ! empty( $where_args['productId'] ) ) {
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$order_ids = $wpdb->get_col(
|
|
$wpdb->prepare(
|
|
"SELECT order_id
|
|
FROM {$wpdb->prefix}woocommerce_order_items
|
|
WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
|
|
AND order_item_type = 'line_item'",
|
|
absint( $where_args['productId'] )
|
|
)
|
|
);
|
|
|
|
// Force WP_Query return empty if don't found any order.
|
|
$args['post__in'] = ! empty( $order_ids ) ? $order_ids : [ 0 ];
|
|
}
|
|
|
|
// Search.
|
|
if ( ! empty( $args['s'] ) ) {
|
|
$order_ids = wc_order_search( $args['s'] );
|
|
if ( ! empty( $order_ids ) ) {
|
|
unset( $args['s'] );
|
|
$args['post__in'] = isset( $args['post__in'] )
|
|
? array_intersect( $order_ids, $args['post__in'] )
|
|
: $order_ids;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter the input fields
|
|
* This allows plugins/themes to hook in and alter what $args should be allowed to be passed
|
|
* from a GraphQL Query to the WP_Query
|
|
*
|
|
* @param array $args The mapped query arguments
|
|
* @param array $where_args Query "where" args
|
|
* @param mixed $source The query results for a query calling this
|
|
* @param array<string, mixed>|null $all_args All of the arguments for the query (not just the "where" args)
|
|
* @param \WPGraphQL\AppContext $context The AppContext object
|
|
* @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
|
|
* @param mixed|string|array $post_type The post type for the query
|
|
*/
|
|
$args = apply_filters(
|
|
'graphql_map_input_fields_to_order_query',
|
|
$args,
|
|
$where_args,
|
|
$this->source,
|
|
$this->args,
|
|
$this->context,
|
|
$this->info,
|
|
$this->post_type
|
|
);
|
|
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Determine whether or not the the offset is valid, i.e the order corresponding to the offset
|
|
* exists. Offset is equivalent to order_id. So this function is equivalent to checking if the
|
|
* post with the given ID exists.
|
|
*
|
|
* @param int $offset The ID of the node used in the cursor offset.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_valid_offset( $offset ) {
|
|
return (bool) \wc_get_order( absint( $offset ) );
|
|
}
|
|
}
|