diff --git a/composer.json b/composer.json index dde66048..5936d4d1 100644 --- a/composer.json +++ b/composer.json @@ -26,8 +26,8 @@ }, "require-dev": { "axepress/wp-graphql-cs": "^2.0.0-beta", - "axepress/wp-graphql-stubs": "~1.16.0", - "php-stubs/woocommerce-stubs": "7.9.0", + "axepress/wp-graphql-stubs": "^1.27.1", + "php-stubs/woocommerce-stubs": "9.1.0", "phpstan/extension-installer": "^1.3", "phpstan/phpdoc-parser": "^1.22.0", "phpstan/phpstan": "^1.10", diff --git a/composer.lock b/composer.lock index be089afe..70d06917 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0a63256d1a0b55f5be349b050cf48c59", + "content-hash": "fe319feb8c0b2d7478695d8dce39b020", "packages": [ { "name": "firebase/php-jwt", @@ -185,16 +185,16 @@ }, { "name": "axepress/wp-graphql-stubs", - "version": "v1.16.0+repack.1", + "version": "v1.27.1", "source": { "type": "git", "url": "https://github.com/AxeWP/wp-graphql-stubs.git", - "reference": "47ce4c7e0c715bea84bc84b675e274b94a8c22f4" + "reference": "84ef51264b142d92c0ac9aa39edf2cce5f0ac3af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AxeWP/wp-graphql-stubs/zipball/47ce4c7e0c715bea84bc84b675e274b94a8c22f4", - "reference": "47ce4c7e0c715bea84bc84b675e274b94a8c22f4", + "url": "https://api.github.com/repos/AxeWP/wp-graphql-stubs/zipball/84ef51264b142d92c0ac9aa39edf2cce5f0ac3af", + "reference": "84ef51264b142d92c0ac9aa39edf2cce5f0ac3af", "shasum": "" }, "require": { @@ -225,7 +225,7 @@ ], "support": { "issues": "https://github.com/AxeWP/wp-graphql-stubs/issues", - "source": "https://github.com/AxeWP/wp-graphql-stubs/tree/v1.16.0+repack.1" + "source": "https://github.com/AxeWP/wp-graphql-stubs/tree/v1.27.1" }, "funding": [ { @@ -233,7 +233,7 @@ "type": "github" } ], - "time": "2023-10-01T17:13:28+00:00" + "time": "2024-07-04T12:37:29+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -315,16 +315,16 @@ }, { "name": "php-stubs/woocommerce-stubs", - "version": "v7.9.0", + "version": "v9.1.0", "source": { "type": "git", "url": "https://github.com/php-stubs/woocommerce-stubs.git", - "reference": "3a2f522e29451490c357af550227795d2b0fc55a" + "reference": "2c95c633362d1f4f531f69e5db63bb19399d8b58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/3a2f522e29451490c357af550227795d2b0fc55a", - "reference": "3a2f522e29451490c357af550227795d2b0fc55a", + "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/2c95c633362d1f4f531f69e5db63bb19399d8b58", + "reference": "2c95c633362d1f4f531f69e5db63bb19399d8b58", "shasum": "" }, "require": { @@ -353,33 +353,34 @@ ], "support": { "issues": "https://github.com/php-stubs/woocommerce-stubs/issues", - "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v7.9.0" + "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v9.1.0" }, - "time": "2023-07-17T22:41:38+00:00" + "time": "2024-07-11T10:55:02+00:00" }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.5.3", + "version": "v6.6.0", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "e611a83292d02055a25f83291a98fadd0c21e092" + "reference": "86e8753e89d59849276dcdd91b9a7dd78bb4abe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/e611a83292d02055a25f83291a98fadd0c21e092", - "reference": "e611a83292d02055a25f83291a98fadd0c21e092", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/86e8753e89d59849276dcdd91b9a7dd78bb4abe2", + "reference": "86e8753e89d59849276dcdd91b9a7dd78bb4abe2", "shasum": "" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "nikic/php-parser": "^4.13", - "php": "^7.4 || ~8.0.0", + "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "5.3", + "phpdocumentor/reflection-docblock": "^5.4.1", "phpstan/phpstan": "^1.10.49", "phpunit/phpunit": "^9.5", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.11" + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", @@ -400,9 +401,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.5.3" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.6.0" }, - "time": "2024-05-08T02:12:31+00:00" + "time": "2024-07-17T08:50:38+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -776,16 +777,16 @@ }, { "name": "phpstan/extension-installer", - "version": "1.3.1", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/phpstan/extension-installer.git", - "reference": "f45734bfb9984c6c56c4486b71230355f066a58a" + "reference": "f6b87faf9fc7978eab2f7919a8760bc9f58f9203" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/f45734bfb9984c6c56c4486b71230355f066a58a", - "reference": "f45734bfb9984c6c56c4486b71230355f066a58a", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/f6b87faf9fc7978eab2f7919a8760bc9f58f9203", + "reference": "f6b87faf9fc7978eab2f7919a8760bc9f58f9203", "shasum": "" }, "require": { @@ -814,22 +815,22 @@ "description": "Composer plugin for automatic installation of PHPStan extensions", "support": { "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.3.1" + "source": "https://github.com/phpstan/extension-installer/tree/1.4.1" }, - "time": "2023-05-24T08:59:17+00:00" + "time": "2024-06-10T08:20:49+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -861,22 +862,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-05-06T12:04:23+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "phpstan/phpstan", - "version": "1.11.1", + "version": "1.11.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e524358f930e41a2b4cca1320e3b04fc26b39e0b" + "reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e524358f930e41a2b4cca1320e3b04fc26b39e0b", - "reference": "e524358f930e41a2b4cca1320e3b04fc26b39e0b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec", + "reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec", "shasum": "" }, "require": { @@ -921,20 +922,20 @@ "type": "github" } ], - "time": "2024-05-15T08:00:59+00:00" + "time": "2024-07-24T07:01:22+00:00" }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.18", + "version": "v2.11.19", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0" + "reference": "bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0", - "reference": "ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1", + "reference": "bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1", "shasum": "" }, "require": { @@ -979,7 +980,7 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2024-04-13T16:42:46+00:00" + "time": "2024-06-26T20:08:34+00:00" }, { "name": "slevomat/coding-standard", @@ -1048,16 +1049,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.0", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "57e09801c2fbae2d257b8b75bebb3deeb7e9deb2" + "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/57e09801c2fbae2d257b8b75bebb3deeb7e9deb2", - "reference": "57e09801c2fbae2d257b8b75bebb3deeb7e9deb2", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", + "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", "shasum": "" }, "require": { @@ -1124,20 +1125,20 @@ "type": "open_collective" } ], - "time": "2024-05-20T08:11:32+00:00" + "time": "2024-07-21T23:26:44+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", "shasum": "" }, "require": { @@ -1184,7 +1185,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" }, "funding": [ { @@ -1200,20 +1201,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.4", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "891d0767855a32c886a439efae090408cc1fa156" + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/891d0767855a32c886a439efae090408cc1fa156", - "reference": "891d0767855a32c886a439efae090408cc1fa156", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", "shasum": "" }, "require": { @@ -1228,7 +1229,8 @@ "php-parallel-lint/php-parallel-lint": "^1.1", "phpstan/phpstan-strict-rules": "^1.2", "phpunit/phpunit": "^8.0 || ^9.0", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8" + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" @@ -1260,9 +1262,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.4" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" }, - "time": "2024-03-21T16:32:59+00:00" + "time": "2024-06-28T22:27:19+00:00" }, { "name": "wp-coding-standards/wpcs", diff --git a/includes/connection/class-customers.php b/includes/connection/class-customers.php index eddaee6a..55707260 100644 --- a/includes/connection/class-customers.php +++ b/includes/connection/class-customers.php @@ -201,7 +201,7 @@ public static function map_input_fields_to_wp_query( $query_args, $where_args, $ * @return array */ public static function upgrade_models( $connection, $resolver ) { - if ( 'customers' === $resolver->get_info()->fieldName ) { // @phpstan-ignore-line + if ( 'customers' === $resolver->get_info()->fieldName ) { $nodes = []; $edges = []; foreach ( $connection['nodes'] as $node ) { diff --git a/includes/connection/class-orders.php b/includes/connection/class-orders.php index e039bcc8..ded8fd07 100644 --- a/includes/connection/class-orders.php +++ b/includes/connection/class-orders.php @@ -99,7 +99,7 @@ public static function register_connections() { * @param \WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver $resolver Connection resolver. * @param \WC_Customer $customer Customer object of querying user. * - * @return array + * @return array|\GraphQL\Deferred */ private static function get_customer_order_connection( $resolver, $customer ) { // If not "billing email" or "ID" set bail early by returning an empty connection. @@ -129,7 +129,7 @@ private static function get_customer_order_connection( $resolver, $customer ) { * @param \WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver $resolver Connection resolver. * @param \WC_Customer $customer Customer object of querying user. * - * @return array + * @return array|\GraphQL\Deferred */ private static function get_customer_refund_connection( $resolver, $customer ) { $empty_results = [ diff --git a/includes/data/connection/class-cart-item-connection-resolver.php b/includes/data/connection/class-cart-item-connection-resolver.php index 56fe1f2b..c176062f 100644 --- a/includes/data/connection/class-cart-item-connection-resolver.php +++ b/includes/data/connection/class-cart-item-connection-resolver.php @@ -64,10 +64,10 @@ public function get_query_args() { /** * 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 $args The inputArgs on the field. - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree. + * @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|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_cart_item_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info ); diff --git a/includes/data/connection/class-coupon-connection-resolver.php b/includes/data/connection/class-coupon-connection-resolver.php index 230e8306..16d4ddd2 100644 --- a/includes/data/connection/class-coupon-connection-resolver.php +++ b/includes/data/connection/class-coupon-connection-resolver.php @@ -159,10 +159,10 @@ public function get_query_args() { /** * 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 $args The inputArgs on the field - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree + * @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|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_coupon_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info ); @@ -178,7 +178,9 @@ public function get_query_args() { * @return \WP_Query */ public function get_query() { - $query = new \WP_Query( $this->query_args ); + /** @var array $query_args */ + $query_args = $this->query_args; + $query = new \WP_Query( $query_args ); if ( isset( $query->query_vars['suppress_filters'] ) && true === $query->query_vars['suppress_filters'] ) { throw new InvariantViolation( __( 'WP_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.', 'wp-graphql-woocommerce' ) ); @@ -237,13 +239,13 @@ public function sanitize_input_fields( array $where_args ) { * 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 $all_args All of the arguments for the query (not just the "where" args) - * @param \WPGraphQL\AppContext $context The AppContext object + * @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|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 + * @param mixed|string|array $post_type The post type for the query */ $args = apply_filters( 'graphql_map_input_fields_to_coupon_query', diff --git a/includes/data/connection/class-downloadable-item-connection-resolver.php b/includes/data/connection/class-downloadable-item-connection-resolver.php index 5fb546d1..a2149602 100644 --- a/includes/data/connection/class-downloadable-item-connection-resolver.php +++ b/includes/data/connection/class-downloadable-item-connection-resolver.php @@ -91,10 +91,10 @@ public function get_query_args() { /** * 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 $args The inputArgs on the field. - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree. + * @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|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_downloadable_item_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info ); diff --git a/includes/data/connection/class-order-connection-resolver.php b/includes/data/connection/class-order-connection-resolver.php index 91e9c821..71633667 100644 --- a/includes/data/connection/class-order-connection-resolver.php +++ b/includes/data/connection/class-order-connection-resolver.php @@ -182,10 +182,10 @@ public function get_query_args() { /** * 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 $args The inputArgs on the field - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree + * @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|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 ); @@ -201,7 +201,9 @@ public function get_query_args() { * @return \WC_Order_Query */ public function get_query() { - $query = new \WC_Order_Query( $this->query_args ); + /** @var array $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.', 'wp-graphql-woocommerce' ) ); @@ -335,13 +337,13 @@ public function sanitize_input_fields( array $where_args ) { * 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 $all_args All of the arguments for the query (not just the "where" args) - * @param \WPGraphQL\AppContext $context The AppContext object + * @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|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 + * @param mixed|string|array $post_type The post type for the query */ $args = apply_filters( 'graphql_map_input_fields_to_order_query', diff --git a/includes/data/connection/class-order-item-connection-resolver.php b/includes/data/connection/class-order-item-connection-resolver.php index 858b2fba..93f47c9f 100644 --- a/includes/data/connection/class-order-item-connection-resolver.php +++ b/includes/data/connection/class-order-item-connection-resolver.php @@ -45,10 +45,10 @@ public function get_query_args() { /** * 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 $args The inputArgs on the field. - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree. + * @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|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_item_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info ); @@ -80,10 +80,10 @@ public function get_query() { /** * Filter the $item_type to allow non-core item types. * - * @param string $item_type Order item type. - * @param mixed $source The source that's passed down the GraphQL queries. - * @param array $args The inputArgs on the field. - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree. + * @param string $item_type Order item type. + * @param mixed $source The source that's passed down the GraphQL queries. + * @param array|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. */ $type = apply_filters( @@ -143,7 +143,7 @@ public function get_query() { // Cache items for later. foreach ( $items as $item ) { - $this->loader->prime( + $this->get_loader()->prime( $item->get_id(), new \WPGraphQL\WooCommerce\Model\Order_Item( $item, $this->source ) ); diff --git a/includes/data/connection/class-product-connection-resolver.php b/includes/data/connection/class-product-connection-resolver.php index 7a2bc803..bc9961df 100644 --- a/includes/data/connection/class-product-connection-resolver.php +++ b/includes/data/connection/class-product-connection-resolver.php @@ -10,7 +10,7 @@ namespace WPGraphQL\WooCommerce\Data\Connection; -use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer; +use Automattic\WooCommerce\StoreApi\Utilities\ProductQuery; use WPGraphQL\Data\Connection\AbstractConnectionResolver; use WPGraphQL\Utils\Utils; use WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce; @@ -36,9 +36,9 @@ class Product_Connection_Resolver extends AbstractConnectionResolver { /** * The instance of the class that helps filtering with the product attributes lookup table. * - * @var \Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer + * @var \Automattic\WooCommerce\StoreApi\Utilities\ProductQuery */ - private $filterer; // @phpstan-ignore-line + private $products_query; /** * Refund_Connection_Resolver constructor. @@ -52,7 +52,7 @@ public function __construct( $source, $args, $context, $info ) { // @codingStandardsIgnoreLine. $this->post_type = ['product']; - $this->filterer = wc_get_container()->get( Filterer::class ); // @phpstan-ignore-line + $this->products_query = new ProductQuery(); /** * Call the parent construct to setup class data @@ -155,7 +155,7 @@ public function get_query_args() { /** * If the query contains search default the results to */ - if ( isset( $query_args['search'] ) && ! empty( $query_args['search'] ) ) { + if ( ! empty( $query_args['search'] ) && empty( $query_args['orderby'] ) ) { /** * Don't order search results by title (causes funky issues with cursors) */ @@ -167,6 +167,54 @@ public function get_query_args() { $query_args = array_merge( $query_args, \WC()->query->get_catalog_ordering_args( 'menu_order', isset( $last ) ? 'ASC' : 'DESC' ) ); } + $has_offset = isset( $last ) ? $this->get_before_offset() : $this->get_after_offset(); + + $offset_product = null; + if ( $has_offset ) { + /** @var \WPGraphQL\WooCommerce\Model\Product|null $offset_model */ + $offset_model = $this->get_loader()->load( $has_offset ); + + /** @var \WC_Product|\WC_Product_Variable|null $offset_product */ + $offset_product = $offset_model ? $offset_model->as_WC_Data() : null; + } + + if ( $offset_product && 'price' === $query_args['orderby'] ) { + /** @var array|float|null $price */ + $price = is_a( $offset_product, 'WC_Product_Variable' ) + ? $offset_product->get_variation_price() + : $offset_product->get_price(); + if ( 'ASC' === $query_args['order'] ) { + if ( is_array( $price ) ) { + $price = reset( $price ); + } + $query_args['graphql_cursor_compare_by_price_key'] = 'wc_product_meta_lookup.min_price'; + $query_args['graphql_cursor_compare_by_price_value'] = $price; + } else { + if ( is_array( $price ) ) { + $price = end( $price ); + } + $query_args['graphql_cursor_compare_by_price_key'] = 'wc_product_meta_lookup.max_price'; + $query_args['graphql_cursor_compare_by_price_value'] = $price; + } + } + + if ( $offset_product && 'popularity' === $query_args['orderby'] ) { + $query_args['graphql_cursor_compare_by_popularity_value'] = $offset_product->get_total_sales(); + $query_args['graphql_cursor_compare_by_popularity_key'] = 'wc_product_meta_lookup.total_sales'; + } + + if ( $offset_product && 'rating' === $query_args['orderby'] ) { + $query_args['graphql_cursor_compare_by_rating_value'] = $offset_product->get_average_rating(); + $query_args['graphql_cursor_compare_by_rating_key'] = 'wc_product_meta_lookup.average_rating'; + } + + if ( $offset_product && 'comment_count' === $query_args['orderby'] ) { + $query_args['graphql_cursor_compare_by_comment_count_value'] = $offset_product->get_rating_count(); + $query_args['graphql_cursor_compare_by_comment_count_key'] = 'wc_product_meta_lookup.rating_count'; + $query_args['graphql_cursor_compare_by_rating_value'] = $offset_product->get_average_rating(); + $query_args['graphql_cursor_compare_by_rating_key'] = 'wc_product_meta_lookup.average_rating'; + } + /** * NOTE: Only IDs should be queried here as the Deferred resolution will handle * fetching the full objects, either from cache of from a follow-up query to the DB @@ -178,7 +226,7 @@ public function get_query_args() { * * @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 $args The inputArgs on the field + * @param array|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 */ @@ -189,83 +237,19 @@ public function get_query_args() { * {@inheritDoc} */ public function get_query() { - // Run query and add product query filters. - $wp_query = new \WP_Query(); - $wp_query->query_vars = wp_parse_args( $this->query_args ); - add_filter( 'posts_clauses', [ $this, 'product_query_post_clauses' ], 10, 2 ); - - return $wp_query; - } - - /** - * Filter the product query and apply WC's custom clauses. - * - * @param array $args The query clauses. - * @param \WP_Query $wp_query The WP_Query object. - * - * @return array - */ - public function product_query_post_clauses( $args, $wp_query ) { - if ( 'product_query' !== $wp_query->get( 'wc_query' ) ) { - return $args; - } - - $args = $this->price_filter_post_clauses( $args, $wp_query ); - $args = $this->filterer->filter_by_attribute_post_clauses( $args, $wp_query, [] ); // @phpstan-ignore-line - - return $args; - } - - /** - * Custom query used to filter products by price. - * - * @param array $args SQL clauses. - * @param \WP_Query $wp_query WP_Query object. - * - * @return array - */ - public function price_filter_post_clauses( $args, $wp_query ) { - global $wpdb; - - $min_price = $wp_query->get( 'min_price' ); - $max_price = $wp_query->get( 'max_price' ); - - // phpcs:disable WordPress.Security.NonceVerification.Recommended - $current_min_price = $min_price ?: 0; - $current_max_price = $max_price ?: PHP_INT_MAX; - // phpcs:enable WordPress.Security.NonceVerification.Recommended + add_filter( 'posts_clauses', [ $this->products_query, 'add_query_clauses' ], 10, 2 ); - /** - * Adjust if the store taxes are not displayed how they are stored. - * Kicks in when prices excluding tax are displayed including tax. - */ - if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { - $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound - $tax_rates = \WC_Tax::get_rates( $tax_class ); - - if ( $tax_rates ) { - $current_min_price -= \WC_Tax::get_tax_total( \WC_Tax::calc_inclusive_tax( $current_min_price, $tax_rates ) ); - $current_max_price -= \WC_Tax::get_tax_total( \WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) ); - } - } - - $args['join'] .= ! strstr( $args['join'], 'wc_product_meta_lookup' ) - ? " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id " - : ''; - $args['where'] .= $wpdb->prepare( - ' AND NOT (%fwc_product_meta_lookup.max_price ) ', - $current_max_price, - $current_min_price - ); - return $args; + return new \WP_Query(); } /** * {@inheritDoc} */ public function get_ids_from_query() { - $ids = $this->query->get_posts(); - remove_filter( 'posts_clauses', [ $this, 'product_query_post_clauses' ], 10 ); + // Run query and get IDs. + $ids = $this->query->query( $this->query_args ); + + remove_filter( 'posts_clauses', [ $this->products_query, 'add_query_clauses' ], 10 ); // If we're going backwards, we need to reverse the array. if ( ! empty( $this->args['last'] ) ) { @@ -332,9 +316,13 @@ public function sanitize_input_fields( array $where_args ) { $default_order = isset( $this->args['last'] ) ? 'ASC' : 'DESC'; $orderby_input = current( $where_args['orderby'] ); - $orderby = $orderby_input['field']; - $order = ! empty( $orderby_input['order'] ) ? $orderby_input['order'] : $default_order; - $query_args = array_merge( $query_args, \WC()->query->get_catalog_ordering_args( $orderby, $order ) ); + $orderby = $orderby_input['field']; + $order = ! empty( $orderby_input['order'] ) ? $orderby_input['order'] : $default_order; + // Set the order to DESC if orderby is popularity. + if ( 'popularity' === $orderby || 'rating' === $orderby ) { + $order = 'DESC'; + } + $query_args = array_merge( $query_args, \wc()->query->get_catalog_ordering_args( $orderby, $order ) ); } if ( isset( $where_args['includeVariations'] ) && $where_args['includeVariations'] ) { @@ -669,15 +657,11 @@ public function sanitize_input_fields( array $where_args ) { ]; } if ( ! empty( $where_args['minPrice'] ) ) { - $query_args['min_price'] = floatval( $where_args['minPrice'] ); - } - - if ( ! empty( $where_args['minPrice'] ) ) { - $query_args['min_price'] = floatval( $where_args['minPrice'] ); + $query_args['min_price'] = str_replace( '.', '', number_format( $where_args['minPrice'], 2 ) ); } if ( ! empty( $where_args['maxPrice'] ) ) { - $query_args['max_price'] = floatval( $where_args['maxPrice'] ); + $query_args['max_price'] = str_replace( '.', '', number_format( $where_args['maxPrice'], 2 ) ); } if ( isset( $where_args['stockStatus'] ) ) { diff --git a/includes/data/connection/class-shipping-method-connection-resolver.php b/includes/data/connection/class-shipping-method-connection-resolver.php index cdd527b3..0d8b2829 100644 --- a/includes/data/connection/class-shipping-method-connection-resolver.php +++ b/includes/data/connection/class-shipping-method-connection-resolver.php @@ -64,7 +64,7 @@ public function get_query() { } foreach ( $methods as $method ) { - $this->loader->prime( $method->id, new Shipping_Method( $method ) ); + $this->get_loader()->prime( $method->id, new Shipping_Method( $method ) ); } // Get shipping method IDs. diff --git a/includes/data/connection/class-tax-class-connection-resolver.php b/includes/data/connection/class-tax-class-connection-resolver.php index dffb93bc..b04f382c 100644 --- a/includes/data/connection/class-tax-class-connection-resolver.php +++ b/includes/data/connection/class-tax-class-connection-resolver.php @@ -74,7 +74,7 @@ public function get_query() { // Cache cart items for later. foreach ( $tax_classes as $tax_class ) { - $this->loader->prime( $tax_class['slug'], $tax_class ); + $this->get_loader()->prime( $tax_class['slug'], $tax_class ); } return wp_list_pluck( $tax_classes, 'slug' ); diff --git a/includes/data/connection/class-tax-rate-connection-resolver.php b/includes/data/connection/class-tax-rate-connection-resolver.php index 9c173c20..59205ea2 100644 --- a/includes/data/connection/class-tax-rate-connection-resolver.php +++ b/includes/data/connection/class-tax-rate-connection-resolver.php @@ -95,10 +95,10 @@ public function get_query_args() { /** * 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 $args The inputArgs on the field - * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree + * @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|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_tax_rate_connection_query_args', $query_args, $this->source, $this->args, $this->context, $this->info ); @@ -114,8 +114,11 @@ public function get_query_args() { public function get_query() { global $wpdb; - if ( ! empty( $this->query_args['where'] ) ) { - $sql_where = $this->query_args['where']; + /** @var array $query_args */ + $query_args = $this->query_args; + + if ( ! empty( $query_args['where'] ) ) { + $sql_where = $query_args['where']; $results = $wpdb->get_results( // @codingStandardsIgnoreStart $wpdb->prepare( @@ -126,8 +129,8 @@ public function get_query() { WHERE {$sql_where} GROUP BY rates.tax_rate_id ORDER BY %s %s", - $this->query_args['orderby'], - $this->query_args['order'] + $query_args['orderby'], + $query_args['order'] ) ); // @codingStandardsIgnoreEnd } else { @@ -136,8 +139,8 @@ public function get_query() { "SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates ORDER BY %s %s", - $this->query_args['orderby'], - $this->query_args['order'] + $query_args['orderby'], + $query_args['order'] ) ); // @codingStandardsIgnoreEnd }//end if diff --git a/includes/data/loader/class-wc-cpt-loader.php b/includes/data/loader/class-wc-cpt-loader.php index 5f350f24..01feb5b8 100644 --- a/includes/data/loader/class-wc-cpt-loader.php +++ b/includes/data/loader/class-wc-cpt-loader.php @@ -74,6 +74,7 @@ public static function resolve_model( $post_type, $id, $fatal = true ) { * @throws \GraphQL\Error\UserError - throws if the post-type is not a valid WooCommerce post-type. */ public function loadKeys( array $keys ) { + /** @var array $keys */ if ( empty( $keys ) ) { return $keys; } @@ -177,7 +178,6 @@ protected function get_model( $entry, $key ) { break; case 'product_variation': case 'shop_refund': - $parent_id = $entry->post_parent; if ( ! empty( $entry->post_parent ) ) { $context->get_loader( 'wc_post' )->load_deferred( $entry->post_parent ); } diff --git a/includes/model/class-product.php b/includes/model/class-product.php index 09ccd4e9..6a1665d1 100644 --- a/includes/model/class-product.php +++ b/includes/model/class-product.php @@ -172,6 +172,7 @@ private function get_variation_price( $pricing_type = '', $raw = false ) { } sort( $prices, SORT_NUMERIC ); + graphql_debug( implode( ', ', $prices ) ); if ( $raw ) { return implode( ', ', $prices ); diff --git a/includes/mutation/class-review-write.php b/includes/mutation/class-review-write.php index 8ad21164..46ff5d29 100644 --- a/includes/mutation/class-review-write.php +++ b/includes/mutation/class-review-write.php @@ -10,7 +10,6 @@ namespace WPGraphQL\WooCommerce\Mutation; -use GraphQL\Error\UserError; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\Model\Comment; @@ -105,9 +104,6 @@ public static function mutate_and_get_payload() { $resolver = CommentCreate::mutate_and_get_payload(); $payload = $resolver( $input, $context, $info ); - if ( is_a( $payload, UserError::class ) ) { - throw $payload; - } // Set product rating upon successful creation of the review. if ( $payload['success'] ) { diff --git a/includes/type/enum/class-products-orderby-enum.php b/includes/type/enum/class-products-orderby-enum.php index f2299ec5..f6e1a046 100644 --- a/includes/type/enum/class-products-orderby-enum.php +++ b/includes/type/enum/class-products-orderby-enum.php @@ -35,23 +35,21 @@ protected static function values() { 'description' => __( 'Order by product\'s current price', 'wp-graphql-woocommerce' ), ], 'REGULAR_PRICE' => [ - 'value' => 'regular_price', - 'description' => __( 'Order by product\'s regular price', 'wp-graphql-woocommerce' ), + 'value' => 'price', + 'description' => __( 'Order by product\'s regular price', 'wp-graphql-woocommerce' ), + 'deprecationReason' => __( 'This field is deprecated and will be removed in a future version. Use "PRICE" instead.', 'wp-graphql-woocommerce' ), ], 'SALE_PRICE' => [ - 'value' => 'sale_price', - 'description' => __( 'Order by product\'s sale price', 'wp-graphql-woocommerce' ), - ], - 'RELEVANCE' => [ - 'value' => 'relevance', - 'description' => __( 'Order by relevance', 'wp-graphql-woocommerce' ), + 'value' => 'price', + 'description' => __( 'Order by product\'s sale price', 'wp-graphql-woocommerce' ), + 'deprecationReason' => __( 'This field is deprecated and will be removed in a future version. Use "PRICE" instead.', 'wp-graphql-woocommerce' ), ], 'POPULARITY' => [ 'value' => 'popularity', 'description' => __( 'Order by product popularity', 'wp-graphql-woocommerce' ), ], 'REVIEW_COUNT' => [ - 'value' => 'popularity', + 'value' => 'comment_count', 'description' => __( 'Order by number of reviews on product', 'wp-graphql-woocommerce' ), ], 'RATING' => [ @@ -59,16 +57,19 @@ protected static function values() { 'description' => __( 'Order by product average rating', 'wp-graphql-woocommerce' ), ], 'ON_SALE_FROM' => [ - 'value' => '_sale_price_dates_from', - 'description' => __( 'Order by date product sale starts', 'wp-graphql-woocommerce' ), + 'value' => 'date', + 'description' => __( 'Order by date product sale starts', 'wp-graphql-woocommerce' ), + 'deprecationReason' => __( 'This field is deprecated and will be removed in a future version.', 'wp-graphql-woocommerce' ), ], 'ON_SALE_TO' => [ - 'value' => '_sale_price_dates_to', - 'description' => __( 'Order by date product sale ends', 'wp-graphql-woocommerce' ), + 'value' => 'date', + 'description' => __( 'Order by date product sale ends', 'wp-graphql-woocommerce' ), + 'deprecationReason' => __( 'This field is deprecated and will be removed in a future version.', 'wp-graphql-woocommerce' ), ], 'TOTAL_SALES' => [ - 'value' => 'total_sales', - 'description' => __( 'Order by total sales of products sold', 'wp-graphql-woocommerce' ), + 'value' => 'popularity', + 'description' => __( 'Order by total sales of products sold', 'wp-graphql-woocommerce' ), + 'deprecationReason' => __( 'This field is deprecated and will be removed in a future version. Use "POPULARITY" instead', 'wp-graphql-woocommerce' ), ], ] ) diff --git a/includes/type/interface/class-product-with-pricing.php b/includes/type/interface/class-product-with-pricing.php index 9ce2fd9c..0e1a4753 100644 --- a/includes/type/interface/class-product-with-pricing.php +++ b/includes/type/interface/class-product-with-pricing.php @@ -61,6 +61,8 @@ public static function get_fields() { // @codingStandardsIgnoreLine. return $source->priceRaw; } else { + graphql_debug( $source->price ); + // @codingStandardsIgnoreLine. return $source->price; } }, diff --git a/includes/type/object/class-order-type.php b/includes/type/object/class-order-type.php index 0d9d144c..354d34e0 100644 --- a/includes/type/object/class-order-type.php +++ b/includes/type/object/class-order-type.php @@ -427,7 +427,7 @@ public static function get_connections( $other_connections = [] ) { * @param \WPGraphQL\AppContext $context AppContext instance. * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. * - * @return array + * @return \GraphQL\Deferred */ public static function resolve_item_connection( $source, array $args, AppContext $context, ResolveInfo $info ) { $resolver = new Order_Item_Connection_Resolver( $source, $args, $context, $info ); diff --git a/includes/type/object/class-root-query.php b/includes/type/object/class-root-query.php index bbe9b1d4..31dd1bea 100644 --- a/includes/type/object/class-root-query.php +++ b/includes/type/object/class-root-query.php @@ -589,6 +589,7 @@ public static function register_fields() { ], 'description' => __( 'Statistics for a product taxonomy query', 'wp-graphql-woocommerce' ), 'resolve' => static function ( $_, $args ) { + /** @var array $data */ $data = [ 'min_price' => null, 'max_price' => null, @@ -596,7 +597,7 @@ public static function register_fields() { 'stock_status_counts' => null, 'rating_counts' => null, ]; - $filters = new ProductQueryFilters(); // @phpstan-ignore-line + $filters = new ProductQueryFilters(); // Process client-side filters. $request = Collection_Stats_Type::prepare_rest_request( $args['where'] ?? [] ); @@ -633,7 +634,8 @@ public static function register_fields() { $filter_request->set_param( 'min_price', null ); $filter_request->set_param( 'max_price', null ); - $price_results = $filters->get_filtered_price( $filter_request ); // @phpstan-ignore-line + /** @var object{min_price: float, max_price: float} */ + $price_results = $filters->get_filtered_price( $filter_request ); $data['min_price'] = $price_results->min_price; $data['max_price'] = $price_results->max_price; } @@ -645,7 +647,7 @@ public static function register_fields() { * @var \WP_REST_Request $filter_request */ $filter_request = clone $request; - $counts = $filters->get_stock_status_counts( $filter_request ); // @phpstan-ignore-line + $counts = $filters->get_stock_status_counts( $filter_request ); $data['stock_status_counts'] = []; @@ -693,7 +695,7 @@ static function ( $query ) use ( $taxonomy ) { } $filter_request->set_param( 'attributes', $filter_attributes ); - $counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] ); // @phpstan-ignore-line + $counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] ); $data['attribute_counts'][ $taxonomy ] = []; foreach ( $counts as $key => $value ) { @@ -707,7 +709,7 @@ static function ( $query ) use ( $taxonomy ) { } if ( ! empty( $taxonomy__and_queries ) ) { - $counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries ); // @phpstan-ignore-line + $counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries ); foreach ( $taxonomy__and_queries as $taxonomy ) { $data['attribute_counts'][ $taxonomy ] = []; @@ -729,7 +731,7 @@ static function ( $query ) use ( $taxonomy ) { * @var \WP_REST_Request $filter_request */ $filter_request = clone $request; - $counts = $filters->get_rating_counts( $filter_request ); // @phpstan-ignore-line + $counts = $filters->get_rating_counts( $filter_request ); $data['rating_counts'] = []; foreach ( $counts as $key => $value ) { diff --git a/tests/functional/CartTransactionQueueCest.php b/tests/functional/CartTransactionQueueCest.php index b9499ea1..d6f730f7 100644 --- a/tests/functional/CartTransactionQueueCest.php +++ b/tests/functional/CartTransactionQueueCest.php @@ -5,7 +5,8 @@ class CartTransactionQueueCest { private $product_catalog; - public function _before( FunctionalTester $I ) { + public function _before( FunctionalTester $I, $scenario ) { + $scenario->skip( 'This test is unstable' ); // Create Products $this->product_catalog = $I->getCatalog(); } diff --git a/tests/wpunit/ProductQueriesTest.php b/tests/wpunit/ProductQueriesTest.php index 606fcdf2..b66452e7 100644 --- a/tests/wpunit/ProductQueriesTest.php +++ b/tests/wpunit/ProductQueriesTest.php @@ -439,479 +439,6 @@ public function testProductQueryAndIds() { $this->assertQuerySuccessful( $response, $expected ); } - public function testProductsQueryAndWhereArgs() { - $category_3 = $this->factory->product->createProductCategory( 'category-three' ); - $category_4 = $this->factory->product->createProductCategory( 'category-four' ); - $product_ids = [ - $this->factory->product->createSimple( - [ - 'slug' => 'test-product-1', - 'price' => 6000, - 'regular_price' => 6000, - ] - ), - $this->factory->product->createSimple( - [ - 'price' => 2, - 'regular_price' => 2, - 'category_ids' => [ $category_3, $category_4 ], - ] - ), - $this->factory->product->createSimple( - [ - 'featured' => 'true', - 'category_ids' => [ $category_3 ], - ] - ), - $this->factory->product->createExternal(), - $this->factory->product->createSimple( - [ - 'price' => 200, - 'regular_price' => 300, - 'sale_price' => 200, - 'date_on_sale_from' => ( new \DateTime( 'yesterday' ) )->format( 'Y-m-d H:i:s' ), - 'date_on_sale_to' => ( new \DateTime( 'tomorrow' ) )->format( 'Y-m-d H:i:s' ), - 'stock_status' => 'outofstock', - ] - ), - ]; - - $query = ' - query ( - $slugIn: [String], - $status: String, - $category: String, - $categoryIn: [String], - $categoryNotIn: [String], - $categoryId: Int, - $categoryIdIn: [Int] - $categoryIdNotIn: [Int] - $type: ProductTypesEnum, - $typeIn: [ProductTypesEnum], - $typeNotIn: [ProductTypesEnum], - $featured: Boolean, - $maxPrice: Float, - $orderby: [ProductsOrderbyInput] - $taxonomyFilter: ProductTaxonomyInput - $include: [Int] - $exclude: [Int] - $stockStatus: [StockStatusEnum] - ) { - products( where: { - slugIn: $slugIn, - status: $status, - category: $category, - categoryIn: $categoryIn, - categoryNotIn: $categoryNotIn, - categoryId: $categoryId, - categoryIdIn: $categoryIdIn, - categoryIdNotIn: $categoryIdNotIn, - type: $type, - typeIn: $typeIn, - typeNotIn: $typeNotIn, - featured: $featured, - maxPrice: $maxPrice, - orderby: $orderby, - taxonomyFilter: $taxonomyFilter - include: $include - exclude: $exclude - stockStatus: $stockStatus - } ) { - nodes { - id - ... on ProductWithPricing { - databaseId - price - } - ... on InventoriedProduct { - stockStatus - } - } - } - } - '; - - $all_expected_product_nodes = array_map( - function ( $product_id ) { - return $this->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_id ) ) ] - ); - }, - $product_ids - ); - - /** - * Assertion One - * - * Tests query with no arguments, and expect all products to be returned. - */ - $response = $this->graphql( compact( 'query' ) ); - $this->assertQuerySuccessful( $response, $all_expected_product_nodes ); - - /** - * Assertion Two - * - * Tests query with "slug" where argument, and expect the product with - * the slug "test-product-1" to be returned. - */ - $variables = [ 'slugIn' => [ 'test-product-1' ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return 'test-product-1' === $product->get_slug(); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Three - * - * Tests query with "status" where argument, and expect the products with - * a status of "pending" to be returned, which there are none among the test - * product with that status. - */ - $variables = [ 'status' => 'pending' ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ $this->expectedField( 'products.nodes', [] ) ]; - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Four - * - * Tests query with "type" where argument, and expect only "simple" products - * to be returned. - */ - $variables = [ 'type' => 'SIMPLE' ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return 'simple' === $product->get_type(); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Five - * - * Tests query with "typeIn" where argument, and expect only "simple" products - * to be returned. - */ - $variables = [ 'typeIn' => [ 'SIMPLE' ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - // No need to reassign the $expected for this assertion. - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Six - * - * Tests query with "typeNotIn" where argument, and expect all types of products - * with except "simple" to be returned. - */ - $variables = [ 'typeNotIn' => [ 'SIMPLE' ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return 'simple' !== $product->get_type(); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Seven - * - * Tests query with "featured" where argument, expect only featured products - * to be returned. - */ - $variables = [ 'featured' => true ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return $product->get_featured(); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Eight - * - * Tests query with "maxPrice" where argument, and expect all product - * with a price of 10.00+ to be returned. - */ - $variables = [ 'maxPrice' => 10.00 ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return 10.00 >= floatval( $product->get_price() ); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Nine - * - * Tests query with "orderby" where argument, and expect products to - * be return in descending order by "price". - */ - $variables = [ - 'orderby' => [ - [ - 'field' => 'PRICE', - 'order' => 'DESC', - ], - ], - ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - - $expected = [ - $this->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[0] ) ) ], - 0 - ), - $this->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[1] ) ) ], - 4 - ), - ]; - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Ten - * - * Tests query with "category" where argument, and expect products in - * the "category-three" category to be returned. - */ - $variables = [ 'category' => 'category-three' ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids, $category_3 ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return in_array( $category_3, $product->get_category_ids(), true ); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - $this->clearLoaderCache( 'wc_post' ); - - /** - * Assertion Eleven - * - * Tests query with "categoryIn" where argument, and expect products in - * the "category-three" category to be returned. - */ - $variables = [ 'categoryIn' => [ 'category-three' ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - // No need to reassign the $expected for this assertion. - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Twelve - * - * Tests query with "categoryId" where argument, and expect products in - * the "category-three" category to be returned. - */ - $variables = [ 'categoryId' => $category_3 ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - // No need to reassign the $expected for this assertion either. - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Thirteen - * - * Tests query with "categoryNotIn" where argument, and expect all products - * except products in the "category-four" category to be returned. - */ - $variables = [ 'categoryNotIn' => [ 'category-four' ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids, $category_4 ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return ! in_array( $category_4, $product->get_category_ids(), true ); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Fourteen - * - * Tests query with "categoryIdNotIn" where argument, and expect all products - * except products in the "category-four" category to be returned. - */ - $variables = [ 'categoryIdNotIn' => [ $category_4 ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - // No need to reassign the $expected for this assertion. - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Fifteen - * - * Tests query with "categoryIdIn" where argument, and expect products in - * the "category-four" category to be returned. - */ - $variables = [ 'categoryIdIn' => [ $category_4 ] ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids, $category_4 ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return in_array( $category_4, $product->get_category_ids(), true ); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion Sixteen - * - * Tests "taxonomyFilter" where argument - */ - $variables = [ - 'taxonomyFilter' => [ - 'relation' => 'AND', - 'filters' => [ - [ - 'taxonomy' => 'PRODUCT_CAT', - 'terms' => [ 'category-three' ], - ], - [ - 'taxonomy' => 'PRODUCT_CAT', - 'terms' => [ 'category-four' ], - 'operator' => 'NOT_IN', - ], - ], - ], - ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = array_filter( - $all_expected_product_nodes, - static function ( $node, $index ) use ( $product_ids, $category_4, $category_3 ) { - $product = \wc_get_product( $product_ids[ $index ] ); - return ! in_array( $category_4, $product->get_category_ids(), true ) - && in_array( $category_3, $product->get_category_ids(), true ); - }, - ARRAY_FILTER_USE_BOTH - ); - - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion 17-18 - * - * Tests "include" where argument - */ - $variables = [ - 'include' => [ $product_ids[0] ], - ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[0] ) ) ] - ), - ]; - $this->assertQuerySuccessful( $response, $expected ); - - $variables = [ - 'include' => [ 1000 ], - ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->expectedField( - 'products.nodes', - self::IS_FALSY - ), - ]; - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion 19-20 - * - * Tests "exclude" where argument - */ - $variables = [ - 'exclude' => [ $product_ids[0] ], - ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->not()->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[0] ) ) ] - ), - ]; - $this->assertQuerySuccessful( $response, $expected ); - - $variables = [ 'exclude' => $product_ids ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->expectedField( - 'products.nodes', - self::IS_FALSY - ), - ]; - $this->assertQuerySuccessful( $response, $expected ); - - /** - * Assertion 21-22 - * - * Tests "stockStatus" where argument - */ - $variables = [ 'stockStatus' => 'IN_STOCK' ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->not()->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[4] ) ) ] - ), - ]; - $this->assertQuerySuccessful( $response, $expected ); - - $variables = [ 'stockStatus' => 'OUT_OF_STOCK' ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->expectedNode( - 'products.nodes', - [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[4] ) ) ], - 0 - ), - ]; - $this->assertQuerySuccessful( $response, $expected ); - } - public function testProductToTermConnection() { $test_category = $this->factory->product->createProductCategory( 'test-product-category-1' ); $test_tag = $this->factory->product->createProductTag( 'test-product-tag-1' ); diff --git a/tests/wpunit/ProductVariationQueriesTest.php b/tests/wpunit/ProductVariationQueriesTest.php index e7fdda8b..ddb0fb91 100644 --- a/tests/wpunit/ProductVariationQueriesTest.php +++ b/tests/wpunit/ProductVariationQueriesTest.php @@ -170,96 +170,6 @@ public function testVariationQuery() { $this->assertQuerySuccessful( $response, $expected ); } - public function testVariationsQueryAndWhereArgs() { - // Create product variations. - $products = $this->factory->product_variation->createSome( - $this->factory->product->createVariable() - ); - $variation_id = $products['variations'][0]; - $id = $this->toRelayId( 'product', $products['product'] ); - $product = wc_get_product( $products['product'] ); - $variations = $products['variations']; - $prices = $product->get_variation_prices( true ); - - $query = ' - query ( - $id: ID!, - $minPrice: Float, - $parent: Int, - $parentIn: [Int], - $parentNotIn: [Int] - ) { - product( id: $id ) { - ... on VariableProduct { - price - regularPrice - salePrice - variations( where: { - minPrice: $minPrice, - parent: $parent, - parentIn: $parentIn, - parentNotIn: $parentNotIn - } ) { - nodes { - id - price - } - } - } - } - } - '; - - /** - * Assertion One - * - * Test query with no arguments - */ - $this->loginAsShopManager(); - $variables = [ 'id' => $id ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[0] ) ), - $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[1] ) ), - $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[2] ) ), - $this->expectedField( - 'product.price', - \wc_graphql_price( current( $prices['price'] ) ) - . ' - ' - . \wc_graphql_price( end( $prices['price'] ) ) - ), - $this->expectedField( - 'product.regularPrice', - \wc_graphql_price( current( $prices['regular_price'] ) ) - . ' - ' - . \wc_graphql_price( end( $prices['regular_price'] ) ) - ), - $this->expectedField( 'product.salePrice', self::IS_NULL ), - ]; - - $this->assertQuerySuccessful( $response, $expected ); - - $this->clearLoaderCache( 'wc_post' ); - - /** - * Assertion Two - * - * Test "minPrice" where argument - */ - $variables = [ - 'id' => $id, - 'minPrice' => 15, - ]; - $response = $this->graphql( compact( 'query', 'variables' ) ); - $expected = [ - $this->not()->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[0] ) ), - $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[1] ) ), - $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[2] ) ), - ]; - - $this->assertQuerySuccessful( $response, $expected ); - } - public function testProductVariationToMediaItemConnections() { // Create product variations. $products = $this->factory->product_variation->createSome( diff --git a/tests/wpunit/ProductsQueriesTest.php b/tests/wpunit/ProductsQueriesTest.php new file mode 100644 index 00000000..5d1ec287 --- /dev/null +++ b/tests/wpunit/ProductsQueriesTest.php @@ -0,0 +1,1077 @@ +factory->product->createSimple([ + 'name' => 'Product Blue', + 'description' => 'A peach description', + 'price' => 100, + 'regular_price' => 100, + 'sale_price' => 90, + 'stock_status' => 'instock', + 'stock_quantity' => 10, + 'reviews_allowed' => true, + 'average_rating' => 4.5, + ]), + $this->factory->product->createSimple([ + 'name' => 'Product Green', + 'description' => 'A turquoise description', + 'sku' => 'green-sku', + 'price' => 200, + 'regular_price' => 200, + 'sale_price' => 180, + 'stock_status' => 'instock', + 'stock_quantity' => 20, + 'reviews_allowed' => true, + 'average_rating' => 4.0, + ]), + $this->factory->product->createSimple([ + 'name' => 'Product Red', + 'description' => 'A maroon description', + 'price' => 300, + 'regular_price' => 300, + 'sale_price' => 270, + 'stock_status' => 'instock', + 'stock_quantity' => 30, + 'reviews_allowed' => true, + 'average_rating' => 3.5, + ]), + $this->factory->product->createSimple([ + 'name' => 'Product Yellow', + 'description' => 'A teal description', + 'price' => 400, + 'regular_price' => 400, + 'sale_price' => 360, + 'stock_status' => 'instock', + 'stock_quantity' => 40, + 'reviews_allowed' => true, + 'average_rating' => 3.0, + ]), + $this->factory->product->createSimple([ + 'name' => 'Product Purple', + 'description' => 'A magenta description', + 'price' => 500, + 'regular_price' => 500, + 'sale_price' => 450, + 'stock_status' => 'instock', + 'stock_quantity' => 50, + 'reviews_allowed' => true, + 'average_rating' => 2.5, + ]), + ]; + + $order_id = $this->factory->order->createNew( + [ + 'payment_method' => 'cod', + ], + [ + 'line_items' => [ + [ + 'product' => $products[0], + 'qty' => 10, + ], + [ + 'product' => $products[1], + 'qty' => 8, + ], + [ + 'product' => $products[2], + 'qty' => 6, + ], + [ + 'product' => $products[3], + 'qty' => 4, + ], + [ + 'product' => $products[4], + 'qty' => 2, + ], + ], + ] + ); + + $order = \wc_get_order( $order_id ); + $order->calculate_totals(); + $order->update_status( 'completed' ); + + wc_update_total_sales_counts( $order_id ); + + $review_one = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[0], + 'comment_content' => 'It worked great!', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_one, 'rating', 5.0 ); + $review_one = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[0], + 'comment_content' => 'It worked great!', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_one, 'rating', 5.0 ); + + $review_two = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[2], + 'comment_content' => 'It was basic', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_two, 'rating', 3.0 ); + + $review_three = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[2], + 'comment_content' => 'Overpriced', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_three, 'rating', 2.0 ); + + $review_four = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[4], + 'comment_content' => 'Overpriced', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_four, 'rating', 3.5 ); + + $review_five = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[4], + 'comment_content' => 'Overpriced and ugly', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_five, 'rating', 2.5 ); + + $review_six = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[1], + 'comment_content' => 'It was cheap!', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_six, 'rating', 4.2 ); + $review_six = $this->factory()->comment->create( + [ + 'comment_author' => 'Customer', + 'comment_author_email' => 'customer@example.com', + 'comment_post_ID' => $products[1], + 'comment_content' => 'It was cheap!', + 'comment_approved' => 1, + 'comment_type' => 'review', + ] + ); + update_comment_meta( $review_six, 'rating', 4.2 ); + + wc_update_product_lookup_tables(); + + return $products; + } + // Tests + public function testProductsQueryAndWhereArgs() { + $category_3 = $this->factory->product->createProductCategory( 'category-three' ); + $category_4 = $this->factory->product->createProductCategory( 'category-four' ); + $product_ids = [ + $this->factory->product->createSimple( + [ + 'slug' => 'test-product-1', + 'price' => 6000, + 'regular_price' => 6000, + ] + ), + $this->factory->product->createSimple( + [ + 'price' => 2, + 'regular_price' => 2, + 'category_ids' => [ $category_3, $category_4 ], + ] + ), + $this->factory->product->createSimple( + [ + 'featured' => 'true', + 'category_ids' => [ $category_3 ], + ] + ), + $this->factory->product->createExternal(), + $this->factory->product->createSimple( + [ + 'price' => 200, + 'regular_price' => 300, + 'sale_price' => 200, + 'date_on_sale_from' => ( new \DateTime( 'yesterday' ) )->format( 'Y-m-d H:i:s' ), + 'date_on_sale_to' => ( new \DateTime( 'tomorrow' ) )->format( 'Y-m-d H:i:s' ), + 'stock_status' => 'outofstock', + ] + ), + ]; + + $query = ' + query ( + $slugIn: [String], + $status: String, + $category: String, + $categoryIn: [String], + $categoryNotIn: [String], + $categoryId: Int, + $categoryIdIn: [Int] + $categoryIdNotIn: [Int] + $type: ProductTypesEnum, + $typeIn: [ProductTypesEnum], + $typeNotIn: [ProductTypesEnum], + $featured: Boolean, + $maxPrice: Float, + $orderby: [ProductsOrderbyInput] + $taxonomyFilter: ProductTaxonomyInput + $include: [Int] + $exclude: [Int] + $stockStatus: [StockStatusEnum] + ) { + products( where: { + slugIn: $slugIn, + status: $status, + category: $category, + categoryIn: $categoryIn, + categoryNotIn: $categoryNotIn, + categoryId: $categoryId, + categoryIdIn: $categoryIdIn, + categoryIdNotIn: $categoryIdNotIn, + type: $type, + typeIn: $typeIn, + typeNotIn: $typeNotIn, + featured: $featured, + maxPrice: $maxPrice, + orderby: $orderby, + taxonomyFilter: $taxonomyFilter + include: $include + exclude: $exclude + stockStatus: $stockStatus + } ) { + nodes { + id + ... on ProductWithPricing { + databaseId + price + } + ... on InventoriedProduct { + stockStatus + } + } + } + } + '; + + $all_expected_product_nodes = array_map( + function ( $product_id ) { + return $this->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_id ) ) ] + ); + }, + $product_ids + ); + + /** + * Assertion One + * + * Tests query with no arguments, and expect all products to be returned. + */ + $response = $this->graphql( compact( 'query' ) ); + $this->assertQuerySuccessful( $response, $all_expected_product_nodes ); + + /** + * Assertion Two + * + * Tests query with "slug" where argument, and expect the product with + * the slug "test-product-1" to be returned. + */ + $variables = [ 'slugIn' => [ 'test-product-1' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return 'test-product-1' === $product->get_slug(); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Three + * + * Tests query with "status" where argument, and expect the products with + * a status of "pending" to be returned, which there are none among the test + * product with that status. + */ + $variables = [ 'status' => 'pending' ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ $this->expectedField( 'products.nodes', [] ) ]; + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Four + * + * Tests query with "type" where argument, and expect only "simple" products + * to be returned. + */ + $variables = [ 'type' => 'SIMPLE' ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return 'simple' === $product->get_type(); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Five + * + * Tests query with "typeIn" where argument, and expect only "simple" products + * to be returned. + */ + $variables = [ 'typeIn' => [ 'SIMPLE' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + // No need to reassign the $expected for this assertion. + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Six + * + * Tests query with "typeNotIn" where argument, and expect all types of products + * with except "simple" to be returned. + */ + $variables = [ 'typeNotIn' => [ 'SIMPLE' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return 'simple' !== $product->get_type(); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Seven + * + * Tests query with "featured" where argument, expect only featured products + * to be returned. + */ + $variables = [ 'featured' => true ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return $product->get_featured(); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Eight + * + * Tests query with "maxPrice" where argument, and expect all product + * with a price of 10.00+ to be returned. + */ + $variables = [ 'maxPrice' => 10.00 ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return 10.00 >= floatval( $product->get_price() ); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Nine + * + * Tests query with "orderby" where argument, and expect products to + * be return in descending order by "price". + */ + $variables = [ + 'orderby' => [ + [ + 'field' => 'PRICE', + 'order' => 'DESC', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + + $expected = [ + $this->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[0] ) ) ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[1] ) ) ], + 4 + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Ten + * + * Tests query with "category" where argument, and expect products in + * the "category-three" category to be returned. + */ + $variables = [ 'category' => 'category-three' ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids, $category_3 ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return in_array( $category_3, $product->get_category_ids(), true ); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + $this->clearLoaderCache( 'wc_post' ); + + /** + * Assertion Eleven + * + * Tests query with "categoryIn" where argument, and expect products in + * the "category-three" category to be returned. + */ + $variables = [ 'categoryIn' => [ 'category-three' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + // No need to reassign the $expected for this assertion. + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Twelve + * + * Tests query with "categoryId" where argument, and expect products in + * the "category-three" category to be returned. + */ + $variables = [ 'categoryId' => $category_3 ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + // No need to reassign the $expected for this assertion either. + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Thirteen + * + * Tests query with "categoryNotIn" where argument, and expect all products + * except products in the "category-four" category to be returned. + */ + $variables = [ 'categoryNotIn' => [ 'category-four' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids, $category_4 ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return ! in_array( $category_4, $product->get_category_ids(), true ); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Fourteen + * + * Tests query with "categoryIdNotIn" where argument, and expect all products + * except products in the "category-four" category to be returned. + */ + $variables = [ 'categoryIdNotIn' => [ $category_4 ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + // No need to reassign the $expected for this assertion. + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Fifteen + * + * Tests query with "categoryIdIn" where argument, and expect products in + * the "category-four" category to be returned. + */ + $variables = [ 'categoryIdIn' => [ $category_4 ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids, $category_4 ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return in_array( $category_4, $product->get_category_ids(), true ); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion Sixteen + * + * Tests "taxonomyFilter" where argument + */ + $variables = [ + 'taxonomyFilter' => [ + 'relation' => 'AND', + 'filters' => [ + [ + 'taxonomy' => 'PRODUCT_CAT', + 'terms' => [ 'category-three' ], + ], + [ + 'taxonomy' => 'PRODUCT_CAT', + 'terms' => [ 'category-four' ], + 'operator' => 'NOT_IN', + ], + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = array_filter( + $all_expected_product_nodes, + static function ( $node, $index ) use ( $product_ids, $category_4, $category_3 ) { + $product = \wc_get_product( $product_ids[ $index ] ); + return ! in_array( $category_4, $product->get_category_ids(), true ) + && in_array( $category_3, $product->get_category_ids(), true ); + }, + ARRAY_FILTER_USE_BOTH + ); + + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion 17-18 + * + * Tests "include" where argument + */ + $variables = [ + 'include' => [ $product_ids[0] ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[0] ) ) ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ + 'include' => [ 1000 ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( + 'products.nodes', + self::IS_FALSY + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion 19-20 + * + * Tests "exclude" where argument + */ + $variables = [ + 'exclude' => [ $product_ids[0] ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->not()->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[0] ) ) ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ 'exclude' => $product_ids ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( + 'products.nodes', + self::IS_FALSY + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Assertion 21-22 + * + * Tests "stockStatus" where argument + */ + $variables = [ 'stockStatus' => 'IN_STOCK' ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->not()->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[4] ) ) ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ 'stockStatus' => 'OUT_OF_STOCK' ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[4] ) ) ], + 0 + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testVariationsQueryAndWhereArgs() { + // Create product variations. + $products = $this->factory->product_variation->createSome( + $this->factory->product->createVariable() + ); + $variation_id = $products['variations'][0]; + $id = $this->toRelayId( 'product', $products['product'] ); + $product = wc_get_product( $products['product'] ); + $variations = $products['variations']; + $prices = $product->get_variation_prices( true ); + + $query = ' + query ( + $id: ID!, + $minPrice: Float, + $parent: Int, + $parentIn: [Int], + $parentNotIn: [Int] + ) { + product( id: $id ) { + ... on VariableProduct { + price + regularPrice + salePrice + variations( where: { + minPrice: $minPrice, + parent: $parent, + parentIn: $parentIn, + parentNotIn: $parentNotIn + } ) { + nodes { + id + price + } + } + } + } + } + '; + + /** + * Assertion One + * + * Test query with no arguments + */ + $this->loginAsShopManager(); + $variables = [ 'id' => $id ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[0] ) ), + $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[1] ) ), + $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[2] ) ), + $this->expectedField( + 'product.price', + \wc_graphql_price( current( $prices['price'] ) ) + . ' - ' + . \wc_graphql_price( end( $prices['price'] ) ) + ), + $this->expectedField( + 'product.regularPrice', + \wc_graphql_price( current( $prices['regular_price'] ) ) + . ' - ' + . \wc_graphql_price( end( $prices['regular_price'] ) ) + ), + $this->expectedField( 'product.salePrice', self::IS_NULL ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + $this->clearLoaderCache( 'wc_post' ); + + /** + * Assertion Two + * + * Test "minPrice" where argument + */ + $variables = [ + 'id' => $id, + 'minPrice' => 15, + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->not()->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[0] ) ), + $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[1] ) ), + $this->expectedField( 'product.variations.nodes.#.id', $this->toRelayId( 'product_variation', $variations[2] ) ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testProductsOrderbyArg() { + // Create products. + $products = $this->createProducts(); + + // Query. + $query = 'query ( + $first: Int + $last: Int + $after: String + $before: String + $orderby: [ProductsOrderbyInput] + ) { + products(first: $first, last: $last, after: $after, before: $before, where: { orderby: $orderby }) { + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + nodes { + id + name + averageRating + reviewCount + ... on ProductWithPricing { + databaseId + price + } + } + } + }'; + + /** + * Assert sorting by price functions correctly. + */ + $variables = [ + 'first' => 2, + 'orderby' => [ + [ + 'field' => 'PRICE', + 'order' => 'ASC', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[0] ) ) + ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[1] ) ) + ], + 1 + ), + ], + 'Failed to sort products by price in ascending order.' + ); + + /** + * Assert sorting by price functions correctly w/ pagination. + */ + $endCursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' ); + $this->logData( $endCursor ); + $variables = [ + 'first' => 2, + 'after' => $endCursor, + 'orderby' => [ + [ + 'field' => 'PRICE', + 'order' => 'ASC', + ], + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[2] ) ) + ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[3] ) ) + ], + 1 + ), + ], + 'Failed to sort products by price in ascending order with pagination.' + ); + + /** + * Assert sorting by popularity functions correctly. + */ + $variables = [ + 'first' => 2, + 'orderby' => [ + [ + 'field' => 'POPULARITY', + 'order' => 'DESC', + ], + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[0] ) ) + ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[1] ) ) + ], + 1 + ), + ], + 'Failed to sort products by popularity in ascending order.' + ); + + /** + * Assert sorting by popularity functions correctly w/ pagination. + */ + $endCursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' ); + $variables = [ + 'first' => 2, + 'after' => $endCursor, + 'orderby' => [ + [ + 'field' => 'POPULARITY', + 'order' => 'DESC', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[2] ) ) + ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[3] ) ) + ], + 1 + ), + ], + 'Failed to sort products by popularity in ascending order with pagination.' + ); + + /** + * Assert sorting by rating functions correctly. + */ + $variables = [ + 'first' => 2, + 'orderby' => [ + [ + 'field' => 'RATING', + 'order' => 'DESC', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[0] ) ) + ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[1] ) ) + ], + 1 + ), + ], + 'Failed to sort products by rating in ascending order.' + ); + + /** + * Assert sorting by rating functions correctly w/ pagination. + */ + $endCursor = $this->lodashGet( $response, 'data.products.pageInfo.endCursor' ); + $variables = [ + 'first' => 2, + 'after' => $endCursor, + 'orderby' => [ + [ + 'field' => 'RATING', + 'order' => 'DESC', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[4] ) ) + ], + 0 + ), + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[2] ) ) + ], + 1 + ), + ], + 'Failed to sort products by rating in ascending order with pagination.' + ); + } + + public function testProductsSearchArg() { + // Create products. + $products = $this->createProducts(); + + // Query. + $query = 'query ( + $after: String, + $search: String + ) { + products(first: 2, after: $after, where: { search: $search }) { + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + nodes { + id + name + ... on ProductWithPricing { + databaseId + price + } + } + } + }'; + + /** + * Assert search by product title functions correctly. + */ + $variables = [ + 'search' => 'Green', + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[1] ) ) + ], + 0 + ), + ], + 'Failed to search products by product title.' + ); + + /** + * Assert search by product sku. + */ + $variables = [ + 'search' => 'green-sku', + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( + $response, + [ + $this->expectedNode( + 'products.nodes', + [ + $this->expectedField( 'id', $this->toRelayId( 'product', $products[1] ) ) + ], + 0 + ), + ], + 'Failed to search products by product description content.' + ); + } +}