From 22331ee1bd02b4e51d65f22aebf5b1ac72fb7a7a Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 7 May 2024 11:00:01 -0400 Subject: [PATCH] fix: WPGraphQL v1.24.x support added --- bin/_lib.sh | 2 +- composer.lock | 291 ++++++--- includes/class-type-registry.php | 1 + includes/class-wp-graphql-woocommerce.php | 1 + includes/connection/class-products.php | 14 +- .../class-product-connection-resolver.php | 148 +++-- .../class-collection-stats-query-input.php | 2 +- .../class-collection-stats-where-args.php | 4 +- .../class-product-attribute-query-input.php | 38 ++ .../object/class-collection-stats-type.php | 92 ++- includes/type/object/class-root-query.php | 93 ++- tests/_support/Factory/ProductFactory.php | 51 +- tests/_support/Helper/Wpunit.php | 4 +- .../_support/Helper/crud-helpers/product.php | 4 +- .../_support/TestCase/WooGraphQLTestCase.php | 1 + tests/wpunit/CartMutationsTest.php | 4 +- tests/wpunit/CollectionStatsQueryTest.php | 601 +++++++++++++++++- tests/wpunit/ConnectionPaginationTest.php | 10 +- tests/wpunit/CustomerQueriesTest.php | 4 +- tests/wpunit/ProductQueriesTest.php | 239 ++++++- tests/wpunit/ProductVariationQueriesTest.php | 1 + .../wpunit/VariationAttributeQueriesTest.php | 1 - 22 files changed, 1379 insertions(+), 227 deletions(-) create mode 100644 includes/type/input/class-product-attribute-query-input.php diff --git a/bin/_lib.sh b/bin/_lib.sh index 3bccc54fd..ab5b99e4e 100755 --- a/bin/_lib.sh +++ b/bin/_lib.sh @@ -71,7 +71,7 @@ install_local_test_library() { codeception/module-asserts:* \ codeception/module-rest:* \ codeception/util-universalframework:^1.0 \ - wp-graphql/wp-graphql-testcase:^2.3 \ + wp-graphql/wp-graphql-testcase \ stripe/stripe-php \ fakerphp/faker diff --git a/composer.lock b/composer.lock index 886ba8025..dfc214d7a 100644 --- a/composer.lock +++ b/composer.lock @@ -127,16 +127,16 @@ }, { "name": "axepress/wp-graphql-cs", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "source": { "type": "git", "url": "https://github.com/AxeWP/WPGraphQL-Coding-Standards.git", - "reference": "88b27c5216716fdd3193ddfe33cad04ab1774b5c" + "reference": "764056aacbf67fd9a318d8dc7a3b9107102f974e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AxeWP/WPGraphQL-Coding-Standards/zipball/88b27c5216716fdd3193ddfe33cad04ab1774b5c", - "reference": "88b27c5216716fdd3193ddfe33cad04ab1774b5c", + "url": "https://api.github.com/repos/AxeWP/WPGraphQL-Coding-Standards/zipball/764056aacbf67fd9a318d8dc7a3b9107102f974e", + "reference": "764056aacbf67fd9a318d8dc7a3b9107102f974e", "shasum": "" }, "require": { @@ -181,7 +181,7 @@ "issues": "https://github.com/AxeWP/WPGraphQL-Coding-Standards/issues", "source": "https://github.com/AxeWP/WPGraphQL-Coding-Standards" }, - "time": "2023-11-05T13:36:28+00:00" + "time": "2024-04-05T17:27:39+00:00" }, { "name": "axepress/wp-graphql-stubs", @@ -664,25 +664,27 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.3.2", + "version": "v6.5.2", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "f22b00cacd3b9addc2b07ff48290084503c48574" + "reference": "379f17a90c01498d4c99a0d15aab6e7aa6a2c840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f22b00cacd3b9addc2b07ff48290084503c48574", - "reference": "f22b00cacd3b9addc2b07ff48290084503c48574", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/379f17a90c01498d4c99a0d15aab6e7aa6a2c840", + "reference": "379f17a90c01498d4c99a0d15aab6e7aa6a2c840", "shasum": "" }, "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "nikic/php-parser": "^4.13", "php": "^7.4 || ~8.0.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpstan": "^1.10.12", - "phpunit/phpunit": "^9.5" + "phpdocumentor/reflection-docblock": "5.3", + "phpstan/phpstan": "^1.10.49", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.11" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", @@ -703,9 +705,9 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.3.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.5.2" }, - "time": "2023-10-14T10:08:05+00:00" + "time": "2024-04-14T17:30:14+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -771,28 +773,28 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "paragonie/random_compat": "dev-master", "paragonie/sodium_compat": "dev-master" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -822,22 +824,37 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2022-10-25T01:46:02+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:30:46+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.4", + "version": "2.1.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" + "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", - "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", "shasum": "" }, "require": { @@ -845,10 +862,10 @@ "phpcompatibility/phpcompatibility-paragonie": "^1.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -877,35 +894,50 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" }, - "time": "2022-10-24T09:00:36+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T21:37:59+00:00" }, { "name": "phpcsstandards/phpcsextra", - "version": "1.1.2", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "746c3190ba8eb2f212087c947ba75f4f5b9a58d5" + "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/746c3190ba8eb2f212087c947ba75f4f5b9a58d5", - "reference": "746c3190ba8eb2f212087c947ba75f4f5b9a58d5", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.8", - "squizlabs/php_codesniffer": "^3.7.1" + "phpcsstandards/phpcsutils": "^1.0.9", + "squizlabs/php_codesniffer": "^3.8.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcsstandards/phpcsdevcs": "^1.1.6", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -940,35 +972,50 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", "source": "https://github.com/PHPCSStandards/PHPCSExtra" }, - "time": "2023-09-20T22:06:18+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2023-12-08T16:49:07+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.8", + "version": "1.0.11", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "69465cab9d12454e5e7767b9041af0cd8cd13be7" + "reference": "c457da9dabb60eb7106dd5e3c05132b1a6539c6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/69465cab9d12454e5e7767b9041af0cd8cd13be7", - "reference": "69465cab9d12454e5e7767b9041af0cd8cd13be7", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c457da9dabb60eb7106dd5e3c05132b1a6539c6a", + "reference": "c457da9dabb60eb7106dd5e3c05132b1a6539c6a", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.7.1 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.9.0 || 4.0.x-dev@dev" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -1013,9 +1060,24 @@ "support": { "docs": "https://phpcsutils.com/", "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", "source": "https://github.com/PHPCSStandards/PHPCSUtils" }, - "time": "2023-07-16T21:39:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-24T11:47:18+00:00" }, { "name": "phpstan/extension-installer", @@ -1063,16 +1125,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.2", + "version": "1.28.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bcad8d995980440892759db0c32acae7c8e79442" + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", - "reference": "bcad8d995980440892759db0c32acae7c8e79442", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", "shasum": "" }, "require": { @@ -1104,22 +1166,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.24.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.28.0" }, - "time": "2023-09-26T12:28:12+00:00" + "time": "2024-04-03T18:51:33+00:00" }, { "name": "phpstan/phpstan", - "version": "1.10.41", + "version": "1.10.67", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c6174523c2a69231df55bdc65b61655e72876d76" + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6174523c2a69231df55bdc65b61655e72876d76", - "reference": "c6174523c2a69231df55bdc65b61655e72876d76", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", "shasum": "" }, "require": { @@ -1162,13 +1224,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2023-11-05T12:57:57+00:00" + "time": "2024-04-16T07:22:02+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2557,16 +2615,16 @@ }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.17", + "version": "v2.11.18", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" + "reference": "ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0", + "reference": "ca242a0b7309e0f9d1f73b236e04ecf4ca3248d0", "shasum": "" }, "require": { @@ -2611,36 +2669,36 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2023-08-05T23:46:11+00:00" + "time": "2024-04-13T16:42:46+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.14.1", + "version": "8.15.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" + "reference": "7d1d957421618a3803b593ec31ace470177d7817" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", "phpstan/phpdoc-parser": "^1.23.1", - "squizlabs/php_codesniffer": "^3.7.1" + "squizlabs/php_codesniffer": "^3.9.0" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.37", + "phpstan/phpstan": "1.10.60", "phpstan/phpstan-deprecation-rules": "1.1.4", - "phpstan/phpstan-phpunit": "1.3.14", - "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" }, "type": "phpcodesniffer-standard", "extra": { @@ -2664,7 +2722,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" }, "funding": [ { @@ -2676,20 +2734,20 @@ "type": "tidelift" } ], - "time": "2023-10-08T07:28:08+00:00" + "time": "2024-03-09T15:20:58+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.2", + "version": "3.9.2", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480", + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480", "shasum": "" }, "require": { @@ -2699,11 +2757,11 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", "extra": { @@ -2718,22 +2776,45 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", "standards", "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2023-02-22T23:07:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-04-23T20:25:34+00:00" }, { "name": "symfony/polyfill-php73", @@ -2813,22 +2894,22 @@ }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.2", + "version": "v1.3.4", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a" + "reference": "891d0767855a32c886a439efae090408cc1fa156" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/b8516ed6bab7ec50aae981698ce3f67f1be2e45a", - "reference": "b8516ed6bab7ec50aae981698ce3f67f1be2e45a", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/891d0767855a32c886a439efae090408cc1fa156", + "reference": "891d0767855a32c886a439efae090408cc1fa156", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.10.30", + "phpstan/phpstan": "^1.10.31", "symfony/polyfill-php73": "^1.12.0" }, "require-dev": { @@ -2869,9 +2950,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.2" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.4" }, - "time": "2023-10-16T17:23:56+00:00" + "time": "2024-03-21T16:32:59+00:00" }, { "name": "theseer/tokenizer", @@ -2925,16 +3006,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.0.1", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" + "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", "shasum": "" }, "require": { @@ -2943,16 +3024,16 @@ "ext-tokenizer": "*", "ext-xmlreader": "*", "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.1.0", - "phpcsstandards/phpcsutils": "^1.0.8", - "squizlabs/php_codesniffer": "^3.7.2" + "phpcsstandards/phpcsextra": "^1.2.1", + "phpcsstandards/phpcsutils": "^1.0.10", + "squizlabs/php_codesniffer": "^3.9.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.0", "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "suggest": { "ext-iconv": "For improved results", @@ -2983,11 +3064,11 @@ }, "funding": [ { - "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", + "url": "https://opencollective.com/php_codesniffer", "type": "custom" } ], - "time": "2023-09-14T07:06:09+00:00" + "time": "2024-03-25T16:39:00+00:00" } ], "aliases": [], diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index c679b54ad..723ed8bfe 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -66,6 +66,7 @@ public function init() { Type\WPInputObject\Collection_Stats_Query_Input::register(); Type\WPInputObject\Collection_Stats_Where_Args::register(); Type\WPInputObject\Product_Attribute_Filter_Input::register(); + Type\WPInputObject\Product_Attribute_Query_Input::register(); /** * Interfaces. diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index bd99096c3..d6b181450 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -300,6 +300,7 @@ private function includes() { require $include_directory_path . 'type/input/class-collection-stats-query-input.php'; require $include_directory_path . 'type/input/class-collection-stats-where-args.php'; require $include_directory_path . 'type/input/class-product-attribute-filter-input.php'; + require $include_directory_path . 'type/input/class-product-attribute-query-input.php'; // Include mutation type class files. require $include_directory_path . 'mutation/class-cart-add-fee.php'; diff --git a/includes/connection/class-products.php b/includes/connection/class-products.php index 951ee44f6..ec9992d62 100644 --- a/includes/connection/class-products.php +++ b/includes/connection/class-products.php @@ -379,13 +379,19 @@ public static function get_connection_args( $extra_args = [] ): array { 'type' => 'Int', 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'wp-graphql-woocommerce' ), ], + 'attributes' => [ + 'type' => 'ProductAttributeQueryInput', + 'description' => __( 'Limit result set to products with selected global attribute queries.', 'wp-graphql-woocommerce' ), + ], 'attribute' => [ - 'type' => 'String', - 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'wp-graphql-woocommerce' ), + 'type' => 'ProductAttributeEnum', + 'description' => __( 'Limit result set to products with a specific global product attribute', 'wp-graphql-woocommerce' ), + 'deprecationReason' => 'Use attributes instead.', ], 'attributeTerm' => [ - 'type' => 'String', - 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'wp-graphql-woocommerce' ), + 'type' => 'String', + 'description' => __( 'Limit result set to products with a specific global product attribute term ID (required an assigned attribute).', 'wp-graphql-woocommerce' ), + 'deprecationReason' => 'Use attributes instead.', ], 'stockStatus' => [ 'type' => [ 'list_of' => 'StockStatusEnum' ], diff --git a/includes/data/connection/class-product-connection-resolver.php b/includes/data/connection/class-product-connection-resolver.php index 0c7bec96d..d922bd90a 100644 --- a/includes/data/connection/class-product-connection-resolver.php +++ b/includes/data/connection/class-product-connection-resolver.php @@ -100,8 +100,9 @@ public function get_query_args() { */ $query_args['post_type'] = $this->post_type; + /** - * Set WC Query + * Set the wc_query to product_query */ $query_args['wc_query'] = 'product_query'; @@ -161,8 +162,7 @@ public function get_query_args() { * Don't order search results by title (causes funky issues with cursors) */ $query_args['search_orderby_title'] = false; - $query_args['orderby'] = 'date'; - $query_args['order'] = isset( $last ) ? 'ASC' : 'DESC'; + $query_args = array_merge( $query_args, \WC()->query->get_catalog_ordering_args( 'relevance', isset( $last ) ? 'ASC' : 'DESC' ) ); } /** @@ -213,8 +213,7 @@ public function get_query_args() { }//end if if ( empty( $query_args['orderby'] ) ) { - $query_args['orderby'] = 'date'; - $query_args['order'] = isset( $last ) ? 'ASC' : 'DESC'; + $query_args = array_merge( $query_args, \WC()->query->get_catalog_ordering_args( 'menu_order', isset( $last ) ? 'ASC' : 'DESC' ) ); } /** @@ -241,64 +240,52 @@ public function get_query_args() { * @return \WC_Query */ public function get_query() { - // Run query and remove hook. - \codecept_debug( $this->query_args ); - $query = new \WP_Query( $this->query_args ); - \WC()->query->product_query( $query ); - return $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', array( $this, 'product_query_post_clauses' ), 10, 2 ); + + return $wp_query; } /** - * Add extra clauses to the product 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. * - * @param array $args Product query clauses. - * @param WP_Query $wp_query The current product query. - * @return array The updated product query clauses array. + * @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, \WC_Query::get_layered_nav_chosen_attributes() ); + $args = $this->filterer->filter_by_attribute_post_clauses( $args, $wp_query, [] ); return $args; } - /** - * Join wc_product_meta_lookup to posts if not already joined. - * - * @param string $sql SQL join. - * @return string - */ - private function append_product_sorting_table_join( $sql ) { - global $wpdb; - - if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { - $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; - } - return $sql; - } - /** * Custom query used to filter products by price. * - * @since 3.6.0 - * - * @param array $args Query args. - * @param WP_Query $wp_query WP_Query object. + * @param array $args Query args. + * @param WP_Query $wp_query WP_Query object. * * @return array */ public function price_filter_post_clauses( $args, $wp_query ) { global $wpdb; - if ( 'product_query' !== $wp_query->get('wc_query') ) { - return $args; - } - $current_max_price = $wp_query->get( 'max_price' ); - $current_min_price = $wp_query->get( 'min_price' ); + $min_price = $wp_query->get( 'min_price' ); + $max_price = $wp_query->get( 'max_price' ); - if ( ! $current_max_price && ! $current_min_price ) { - return $args; - } + // 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 /** * Adjust if the store taxes are not displayed how they are stored. @@ -313,8 +300,10 @@ public function price_filter_post_clauses( $args, $wp_query ) { $current_max_price -= \WC_Tax::get_tax_total( \WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) ); } } - - $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + + $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, @@ -328,6 +317,7 @@ public function price_filter_post_clauses( $args, $wp_query ) { */ public function get_ids_from_query() { $ids = $this->query->get_posts(); + remove_filter( 'posts_clauses', array( $this, 'product_query_post_clauses' ), 10, 2 ); // If we're going backwards, we need to reverse the array. if ( ! empty( $this->args['last'] ) ) { @@ -381,18 +371,20 @@ public function sanitize_input_fields( array $where_args ) { 'maxPrice' => 'max_price', 'stockStatus' => 'stock_status', 'status' => 'post_status', - 'include' => 'include', - 'exclude' => 'exclude', - 'parent' => 'parent', - 'parentIn' => 'parent_in', + 'include' => 'post__in', + 'exclude' => 'post__not_in', + 'parent' => 'post_parent', + 'parentIn' => 'post_parent__in', + 'parentNotIn' => 'post_parent__not_in', 'search' => 'search', ] ); if ( ! empty( $where_args['orderby'] ) ) { + $default_order = isset( $this->args['last'] ) ? 'ASC' : 'DESC'; $orderby_input = current( $where_args['orderby'] ); + $orderby = $orderby_input['field']; - $default_order = isset( $this->args['last'] ) ? 'ASC' : 'DESC'; $order = ! empty( $orderby_input['order'] ) ? $orderby_input['order'] : $default_order; $query_args = array_merge( $query_args, \WC()->query->get_catalog_ordering_args( $orderby, $order ) ); } @@ -508,6 +500,9 @@ public function sanitize_input_fields( array $where_args ) { // Filter by attribute and term. if ( ! empty( $where_args['attribute'] ) && ! empty( $where_args['attributeTerm'] ) ) { + graphql_debug( + __( 'The "attribute" and "attributeTerm" arguments have been deprecated. Please use the "attributes" argument instead.', 'wp-graphql-woocommerce' ), + ); if ( in_array( $where_args['attribute'], \wc_get_attribute_taxonomy_names(), true ) ) { $tax_query[] = [ 'taxonomy' => $where_args['attribute'], @@ -517,6 +512,61 @@ public function sanitize_input_fields( array $where_args ) { } } + // Filter by attributes. + if ( ! empty( $where_args['attributes'] ) && ! empty( $where_args['attributes']['queries'] ) ) { + $attributes = $where_args['attributes']['queries']; + $att_queries = []; + + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['ids'] ) && empty( $attribute['terms'] ) ) { + continue; + } + + if ( ! in_array( $attribute['taxonomy'], \wc_get_attribute_taxonomy_names(), true ) ) { + continue; + } + + $operator = isset( $attribute['operator'] ) ? $attribute['operator'] : 'IN'; + + if ( ! empty( $attribute['terms'] ) ) { + foreach( $attribute['terms'] as $term ) { + $att_queries[] = [ + 'taxonomy' => $attribute['taxonomy'], + 'field' => 'slug', + 'terms' => $term, + 'operator' => $operator, + ]; + } + } + + if ( ! empty( $attribute['ids'] ) ) { + foreach( $attribute['ids'] as $id ) { + $att_queries[] = [ + 'taxonomy' => $attribute['taxonomy'], + 'field' => 'term_id', + 'terms' => $id, + 'operator' => $operator, + ]; + } + } + } + + if ( 1 < count( $att_queries ) ) { + $relation = ! empty( $where_args['attributes']['relation'] ) ? $where_args['attributes']['relation'] : 'AND'; + if ( 'NOT_IN' === $relation ) { + graphql_debug( __( 'The "NOT_IN" relation is not supported for attributes. Please use "IN" or "AND" instead.', 'wp-graphql-woocommerce' ) ); + $relation = 'IN'; + } + + $tax_query[] = [ + 'relation' => $relation, + ...$att_queries, + ]; + } else { + $tax_query = array_merge( $tax_query, $att_queries ); + } + } + if ( empty( $where_args['type'] ) && empty( $where_args['typeIn'] ) && ! empty( $where_args['supportedTypesOnly'] ) && true === $where_args['supportedTypesOnly'] ) { $supported_types = array_keys( WP_GraphQL_WooCommerce::get_enabled_product_types() ); diff --git a/includes/type/input/class-collection-stats-query-input.php b/includes/type/input/class-collection-stats-query-input.php index 518d47b17..b2eeae5ee 100644 --- a/includes/type/input/class-collection-stats-query-input.php +++ b/includes/type/input/class-collection-stats-query-input.php @@ -28,7 +28,7 @@ public static function register() { 'description' => __( 'Product Taxonomy', 'wp-graphql-woocommerce' ), ], 'relation' => [ - 'type' => [ 'non_null' => 'RelationEnum' ], + 'type' => 'RelationEnum', 'description' => __( 'Taxonomy relation to query', 'wp-graphql-woocommerce' ), ], ], diff --git a/includes/type/input/class-collection-stats-where-args.php b/includes/type/input/class-collection-stats-where-args.php index 44a975796..3a2b9f5b7 100644 --- a/includes/type/input/class-collection-stats-where-args.php +++ b/includes/type/input/class-collection-stats-where-args.php @@ -76,8 +76,8 @@ public static function register() { 'description' => __( 'Limit result set to products assigned to a specific group of tag IDs.', 'wp-graphql-woocommerce' ), ], 'attributes' => [ - 'type' => [ 'list_of' => 'ProductAttributeFilterInput' ], - 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'wp-graphql-woocommerce' ), + 'type' => 'ProductAttributeQueryInput', + 'description' => __( 'Limit result set to products with selected global attribute queries.', 'wp-graphql-woocommerce' ), ], 'stockStatus' => [ 'type' => [ 'list_of' => 'StockStatusEnum' ], diff --git a/includes/type/input/class-product-attribute-query-input.php b/includes/type/input/class-product-attribute-query-input.php new file mode 100644 index 000000000..6c4a5fd27 --- /dev/null +++ b/includes/type/input/class-product-attribute-query-input.php @@ -0,0 +1,38 @@ + __( 'Product filter', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'queries' => [ + 'type' => [ 'list_of' => 'ProductAttributeFilterInput' ], + 'description' => __( 'Limit result set to products with selected global attributes.', 'wp-graphql-woocommerce' ), + ], + 'relation' => [ + 'type' => 'AttributeOperatorEnum', + 'description' => __( 'The logical relationship between attributes when filtering across multiple at once.', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-collection-stats-type.php b/includes/type/object/class-collection-stats-type.php index 1a9f0477b..1d544b67b 100644 --- a/includes/type/object/class-collection-stats-type.php +++ b/includes/type/object/class-collection-stats-type.php @@ -306,11 +306,14 @@ public static function prepare_rest_request( array $where_args = [] ) /* @phpsta return $request; } + $request->set_param( 'paged', 0 ); + $request->set_param( 'ignore_sticky_posts', true ); + $key_mapping = [ 'slugIn' => 'slug', 'typeIn' => 'type', 'categoryIdIn' => 'category', - 'tagIn' => 'tag', + 'tagIdIn' => 'tag', 'onSale' => 'on_sale', 'stockStatus' => 'stock_status', 'visibility' => 'catalog_visibility', @@ -318,7 +321,7 @@ public static function prepare_rest_request( array $where_args = [] ) /* @phpsta 'maxPrice' => 'max_price', ]; - $needs_formatting = [ 'attributes', 'categoryIn' ]; + $needs_formatting = [ 'attributes', 'categoryIn', 'tagIn' ]; foreach ( $where_args as $key => $value ) { if ( in_array( $key, $needs_formatting, true ) ) { continue; @@ -345,33 +348,76 @@ static function ( $category ) { } else { $request->set_param( 'category', $category_ids ); } - $request->set_param( 'category_operator', 'and' ); + } + + if ( ! empty( $where_args['tagIn'] ) ) { + $tag_ids = array_map( + static function ( $tag ) { + $term = get_term_by( 'slug', $tag, 'product_tag' ); + if ( $term && ! is_wp_error( $term ) ) { + return $term->term_id; + } + return 0; + }, + $where_args['tagIn'] + ); + $set_tag = $request->get_param( 'tag' ); + if ( ! empty( $set_tag ) ) { + $tag_ids[] = $set_tag; + $request->set_param( 'tag', $tag_ids ); + } else { + $request->set_param( 'tag', $tag_ids ); + } } - if ( ! empty( $where_args['attributes'] ) ) { - $attributes = []; - foreach ( $where_args['attributes'] as $filter ) { - if ( str_starts_with( $filter['taxonomy'], 'pa_' ) ) { - $attribute = []; - $attribute['attribute'] = $filter['taxonomy']; - if ( ! empty( $filter['terms'] ) ) { - $attribute['slug'] = $filter['terms']; - } elseif ( ! empty( $filter['ids'] ) ) { - $attribute['term_id'] = $filter['ids']; + if ( ! empty( $where_args['attributes'] ) && ! empty( $where_args['attributes']['queries'] ) ) { + $attributes = $where_args['attributes']['queries']; + $att_queries = []; + $operator_mapping = [ + 'IN' => 'in', + 'NOT IN' => 'not_in', + 'AND' => 'and', + ]; + + foreach ( $attributes as $filter ) { + if ( empty( $filter['terms'] ) && empty( $filter['ids'] ) ) { + continue; + } + + $operator = ! empty( $filter['operator'] ) ? $operator_mapping[ $filter['operator'] ] : 'in'; + if ( ! empty( $filter['terms'] ) ) { + foreach( $filter['terms'] as $term ) { + $att_queries[] = [ + 'attribute' => $filter['taxonomy'], + 'operator' => $operator, + 'slug' => $term, + ]; } - $attribute['operator'] = ! empty( $filter['operator'] ) ? strtolower( $filter['operator'] ) : 'in'; - $attributes[] = $attribute; - } else { - if ( ! empty( $filter['ids'] ) ) { - continue; + } + + + if ( ! empty( $filter['ids'] ) ) { + foreach( $filter['ids'] as $term_id ) { + $att_queries[] = [ + 'attribute' => $filter['taxonomy'], + 'operator' => $operator, + 'term_id' => $term_id, + ]; } - $taxonomy = $filter['taxonomy']; - $request->set_param( "_unstable_tax_{$taxonomy}", $filter['ids'] ); - $request->set_param( "_unstable_tax_{$taxonomy}_operator", strtolower( $filter['operator'] ) ); } } - if ( ! empty( $attributes ) ) { - $request->set_param( 'attributes', $attributes ); + + if ( ! empty( $att_queries ) ) { + $request->set_param( 'attributes', $att_queries ); + } + + if ( ! empty ( $where_args['attributes']['relation'] ) ) { + $relation = $where_args['attributes']['relation']; + if ( $relation === 'NOT_IN' ) { + graphql_debug( __( 'NOT_IN relation is not supported for attributes queries top-level "relation" field. Use "IN" or "AND" instead.', 'wp-graphql-woocommerce' ) ); + $relation = 'IN'; + } + $request->set_param( 'attributes_relation', $where_args['attributes']['relation'] ); } }//end if diff --git a/includes/type/object/class-root-query.php b/includes/type/object/class-root-query.php index d820d2773..b6003ce34 100644 --- a/includes/type/object/class-root-query.php +++ b/includes/type/object/class-root-query.php @@ -539,7 +539,6 @@ public static function register_fields() { ], 'description' => __( 'Statistics for a product taxonomy query', 'wp-graphql-woocommerce' ), 'resolve' => static function ( $_, $args ) { - $filters = new ProductQueryFilters(); // @phpstan-ignore-line $data = [ 'min_price' => null, 'max_price' => null, @@ -547,6 +546,8 @@ public static function register_fields() { 'stock_status_counts' => null, 'rating_counts' => null, ]; + $filters = new ProductQueryFilters(); // @phpstan-ignore-line + // Process client-side filters. $request = Collection_Stats_Type::prepare_rest_request( $args['where'] ?? [] ); @@ -555,11 +556,17 @@ public static function register_fields() { if ( ! empty( $args['taxonomies'] ) ) { $calculate_attribute_counts = []; foreach ( $args['taxonomies'] as $attribute_to_count ) { - $calculate_attribute_counts[] = [ - 'taxonomy' => $attribute_to_count['taxonomy'], - 'query_type' => strtolower( $attribute_to_count['relation'] ), - ]; + $attribute = [ 'taxonomy' => $attribute_to_count['taxonomy'] ]; + // Set the query type. + if ( ! empty( $attribute_to_count['relation'] ) ) { + $attribute['query_type'] = strtolower( $attribute_to_count['relation'] ); + } + + // Add the attribute to the list of attributes to count. + $calculate_attribute_counts[] = $attribute; } + + // Set the attribute counts to calculate. $request->set_param( 'calculate_attribute_counts', $calculate_attribute_counts ); } @@ -570,18 +577,24 @@ public static function register_fields() { if ( ! empty( $request['calculate_price_range'] ) ) { + /** + * @var $filter_request \WP_REST_Request + */ $filter_request = clone $request; $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 + $price_results = $filters->get_filtered_price( $filter_request ); $data['min_price'] = $price_results->min_price; $data['max_price'] = $price_results->max_price; } if ( ! empty( $request['calculate_stock_status_counts'] ) ) { + /** + * @var $filter_request \WP_REST_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'] = []; @@ -594,28 +607,74 @@ public static function register_fields() { } if ( ! empty( $request['calculate_attribute_counts'] ) ) { + $taxonomy__or_queries = []; + $taxonomy__and_queries = []; foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) { if ( ! isset( $attributes_to_count['taxonomy'] ) ) { continue; } - $taxonomy = $attributes_to_count['taxonomy']; - $counts = $filters->get_attribute_counts( $request, $taxonomy ); // @phpstan-ignore-line + if ( empty( $attributes_to_count['query_type'] ) || 'or' === $attributes_to_count['query_type'] ) { + $taxonomy__or_queries[] = $attributes_to_count['taxonomy']; + } else { + $taxonomy__and_queries[] = $attributes_to_count['taxonomy']; + } + } + + $data['attribute_counts'] = []; + if ( ! empty( $taxonomy__or_queries ) ) { + foreach ( $taxonomy__or_queries as $taxonomy ) { + /** + * @var $filter_request \WP_REST_Request + */ + $filter_request = clone $request; + $filter_attributes = $filter_request->get_param( 'attributes' ); + + if ( ! empty( $filter_attributes ) ) { + $filter_attributes = array_filter( + $filter_attributes, + function ( $query ) use ( $taxonomy ) { + return $query['attribute'] !== $taxonomy; + } + ); + } + + $filter_request->set_param( 'attributes', $filter_attributes ); + $counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] ); + + $data['attribute_counts'][ $taxonomy ] = []; + foreach ( $counts as $key => $value ) { + $data['attribute_counts'][ $taxonomy ][] = (object) [ + 'taxonomy' => $taxonomy, + 'termId' => $key, + 'count' => $value, + ]; + } + } + } - $data['attribute_counts'][ $taxonomy ] = []; - foreach ( $counts as $key => $value ) { - $data['attribute_counts'][ $taxonomy ][] = (object) [ - 'taxonomy' => $taxonomy, - 'termId' => $key, - 'count' => $value, - ]; + if ( ! empty( $taxonomy__and_queries ) ) { + $counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries ); + + foreach ( $taxonomy__and_queries as $taxonomy ) { + $data['attribute_counts'][ $taxonomy ] = []; + foreach ( $counts as $key => $value ) { + $data['attribute_counts'][ $taxonomy ][] = (object) [ + 'taxonomy' => $taxonomy, + 'termId' => $key, + 'count' => $value, + ]; + } } } } if ( ! empty( $request['calculate_rating_counts'] ) ) { + /** + * @var $filter_request \WP_REST_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/_support/Factory/ProductFactory.php b/tests/_support/Factory/ProductFactory.php index dc7d816c1..ee497a911 100644 --- a/tests/_support/Factory/ProductFactory.php +++ b/tests/_support/Factory/ProductFactory.php @@ -91,6 +91,15 @@ public function createSimple( $args = [] ) { return $this->create( $args, $generation_definitions ); } + public function createManySimple( $count = 5, $args = []) { + $products = []; + for ( $i = 0; $i < $count; $i++ ) { + $products[] = $this->createSimple( $args ); + } + + return $products; + } + public function createExternal( $args = [] ) { $name = Dummy::instance()->product(); $price = Dummy::instance()->price( 15, 200 ); @@ -154,7 +163,7 @@ public function createVariable( $args = [] ) { return $this->create( $args, $generation_definitions ); } - public function createAttribute( $raw_name = 'size', $terms = [ 'small' ] ) { + public function createAttribute( $raw_name = 'size', $terms = [ 'small' ], $label = '' ) { global $wpdb, $wc_product_attributes; // Make sure caches are clean. @@ -163,7 +172,11 @@ public function createAttribute( $raw_name = 'size', $terms = [ 'small' ] ) { // These are exported as labels, so convert the label to a name if possible first. $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); - $attribute_name = array_search( $raw_name, $attribute_labels, true ); + $attribute_name = array_search( + ! empty( $label ) ? $label : $raw_name, + $attribute_labels, + true + ); if ( ! $attribute_name ) { $attribute_name = wc_sanitize_taxonomy_name( $raw_name ); @@ -174,16 +187,23 @@ public function createAttribute( $raw_name = 'size', $terms = [ 'small' ] ) { if ( ! $attribute_id ) { $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name ); + unregister_taxonomy( $taxonomy_name ); + $attribute_id = wc_create_attribute( [ - 'name' => $raw_name, - 'slug' => $attribute_name, + 'name' => ! empty( $label ) ? $label : $raw_name, + 'slug' => $raw_name, 'type' => 'select', 'order_by' => 'menu_order', 'has_archives' => 0, ] ); + if ( is_wp_error( $attribute_id ) ) { + codecept_debug( json_encode( $attribute_id, JSON_PRETTY_PRINT ) ); + throw new \Exception( 'Failed to create attribute.' ); + } + // Register as taxonomy. register_taxonomy( $taxonomy_name, @@ -192,7 +212,7 @@ public function createAttribute( $raw_name = 'size', $terms = [ 'small' ] ) { 'woocommerce_taxonomy_args_' . $taxonomy_name, [ 'labels' => [ - 'name' => $raw_name, + 'name' => ! empty( $label ) ? $label : $raw_name, ], 'hierarchical' => false, 'show_ui' => false, @@ -232,6 +252,19 @@ public function createAttribute( $raw_name = 'size', $terms = [ 'small' ] ) { return $return; } + public function createAttributeObject( string $id, string $taxonomy, array $options, int $position = 0, bool $visible = true, bool $variation = false ) { + $attribute = new \WC_Product_Attribute(); + + $attribute->set_id( $id ); + $attribute->set_name( $taxonomy ); + $attribute->set_options( $options ); + $attribute->set_position( $position ); + $attribute->set_visible( $visible ); + $attribute->set_variation( $variation ); + + return $attribute; + } + private function setVariationAttributes( \WC_Product_Variable $product, array $attribute_data = [] ) { $attributes = []; foreach ( $attribute_data as $index => $data ) { @@ -374,4 +407,12 @@ private function slugify( $text ) { return $text; } + + public function deleteAttributes() { + global $wpdb; + + $wpdb->query( + "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies" + ); + } } diff --git a/tests/_support/Helper/Wpunit.php b/tests/_support/Helper/Wpunit.php index 1daa84dc9..b0307f53e 100644 --- a/tests/_support/Helper/Wpunit.php +++ b/tests/_support/Helper/Wpunit.php @@ -30,8 +30,8 @@ public function _initialize() { */ public function _beforeSuite( $settings = null ) { $helper = $this->product(); - $helper->create_attribute( 'size', [ 'small', 'medium', 'large' ] ); - $helper->create_attribute( 'color', [ 'red', 'blue', 'green' ] ); + $helper->create_attribute( 'size', [ 'small', 'medium', 'large' ], 'Product size' ); + $helper->create_attribute( 'color', [ 'red', 'blue', 'green' ], 'Product color' ); codecept_debug( 'ATTRIBUTES_LOADED' ); add_action( 'init_graphql_request', [ self::class, 'shortcode_test_init' ] ); codecept_debug( 'SHORTCODE_INITIALIZED' ); diff --git a/tests/_support/Helper/crud-helpers/product.php b/tests/_support/Helper/crud-helpers/product.php index 39855ffd4..133ac71c0 100644 --- a/tests/_support/Helper/crud-helpers/product.php +++ b/tests/_support/Helper/crud-helpers/product.php @@ -210,7 +210,7 @@ public function create_related( $args = array() ) { ); } - public function create_attribute( $raw_name = 'size', $terms = array( 'small' ) ) { + public function create_attribute( $raw_name = 'size', $terms = array( 'small' ), $label = '' ) { global $wpdb, $wc_product_attributes; // Make sure caches are clean. @@ -248,7 +248,7 @@ public function create_attribute( $raw_name = 'size', $terms = array( 'small' ) 'woocommerce_taxonomy_args_' . $taxonomy_name, array( 'labels' => array( - 'name' => $raw_name, + 'name' => ! empty( $label ) ? $label : $raw_name, ), 'hierarchical' => false, 'show_ui' => false, diff --git a/tests/_support/TestCase/WooGraphQLTestCase.php b/tests/_support/TestCase/WooGraphQLTestCase.php index 0019ac790..541dcf0ca 100644 --- a/tests/_support/TestCase/WooGraphQLTestCase.php +++ b/tests/_support/TestCase/WooGraphQLTestCase.php @@ -69,6 +69,7 @@ public function setUp(): void { public function tearDown(): void { \WC()->cart->empty_cart( true ); + $this->factory->product->deleteAttributes(); // then parent::tearDown(); diff --git a/tests/wpunit/CartMutationsTest.php b/tests/wpunit/CartMutationsTest.php index 1de985588..698b475fe 100644 --- a/tests/wpunit/CartMutationsTest.php +++ b/tests/wpunit/CartMutationsTest.php @@ -42,7 +42,7 @@ public function testAddToCartMutationWithProduct() { $response = $this->graphql( compact( 'query', 'variables' ) ); // Confirm valid response - $this->assertIsValidQueryResponse( $response ); + $this->assertResponseIsValid( $response ); // Get/validate cart item key. $cart_item_key = $this->lodashGet( $response, 'data.addToCart.cartItem.key' ); @@ -115,7 +115,7 @@ public function testAddToCartMutationWithProductVariation() { $response = $this->graphql( compact( 'query', 'variables' ) ); // Confirm valid response - $this->assertIsValidQueryResponse( $response ); + $this->assertResponseIsValid( $response ); // Get/validate cart item key. $cart_item_key = $this->lodashGet( $response, 'data.addToCart.cartItem.key' ); diff --git a/tests/wpunit/CollectionStatsQueryTest.php b/tests/wpunit/CollectionStatsQueryTest.php index ebacd8a32..48d61010a 100644 --- a/tests/wpunit/CollectionStatsQueryTest.php +++ b/tests/wpunit/CollectionStatsQueryTest.php @@ -1,5 +1,7 @@ [ 'attributes' => [ - [ - 'taxonomy' => 'PA_COLOR', - 'terms' => 'red', - 'operator' => 'IN', + 'queries' => [ + [ + 'taxonomy' => 'PA_COLOR', + 'terms' => 'red', + 'operator' => 'IN', + ], ], ], ], @@ -68,7 +72,7 @@ public function testCollectionStatsQuery() { 'collectionStats.attributeCounts', [ $this->expectedField( 'slug', 'PA_COLOR' ), - $this->expectedField( 'label', 'color' ), + $this->expectedField( 'label', 'Product color' ), $this->expectedField( 'name', 'color' ), $this->expectedNode( 'terms', @@ -121,4 +125,591 @@ public function testCollectionStatsQuery() { ]; $this->assertQuerySuccessful( $response, $expected ); } + + public function testCollectionStatsQueryWithWhereArgs() { + // Create product attributes. + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'special', 'normal' ], 'Product type' ); + $normal_term_id = get_term_by( 'slug', 'normal', 'pa_kind' )->term_id; + $special_term_id = get_term_by( 'slug', 'special', 'pa_kind' )->term_id; + + // Create attribute objects. + $kind_attribute_normal_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $normal_term_id ] + ); + + $kind_attribute_special_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $special_term_id ] + ); + + $kind_attribute_both = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $normal_term_id, $special_term_id ] + ); + + // Create taxonomies. + $clothing_category_id = $this->factory->product->createProductCategory( 'clothing' ); + $shoes_tag_id = $this->factory->product->createProductTag( 'shoes' ); + + // Create products. + $post_ids = $this->factory->product->createManySimple( + 20, + [ + 'attributes' => [ $kind_attribute_normal_only ], + 'category_ids' => [ $clothing_category_id ], + 'default_attributes' => [ 'pa_kind' => 'normal' ], + ] + ); + $this->factory->product->createManySimple( + 5, + [ + 'category_ids' => [ $clothing_category_id ], + 'attributes' => [ $kind_attribute_special_only ], + 'default_attributes' => [ 'pa_kind' => 'special' ], + ] + ); + $this->factory->product->createManySimple( + 5, + [ + 'category_ids' => [ $clothing_category_id ], + 'tag_ids' => [ $shoes_tag_id ], + 'attributes' => [ $kind_attribute_special_only ], + 'default_attributes' => [ 'pa_kind' => 'special' ], + ] + ); + $this->factory->product->createManySimple( 3, [ 'attributes' => [ $kind_attribute_both ] ] ); + + + $query = " + query (\$where: CollectionStatsWhereArgs, \$taxonomies: [CollectionStatsQueryInput]) { + collectionStats( + calculateRatingCounts: true + taxonomies: \$taxonomies + where: \$where + ) { + + attributeCounts { + slug + terms { + node { slug } + termId + count + } + } + } + } + "; + + /** + * Query for products with the "clothing" category and confirm correct count. + */ + $variables = [ + 'calculateRatingCounts' => true, + 'where' => [ + 'categoryIdIn' => [ $clothing_category_id ], + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_KIND', + 'relation' => "AND", + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_KIND' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'normal' ), + $this->expectedField( 'count', 20 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'special' ), + $this->expectedField( 'count', 10 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + ], + 0 + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + // Test again with the "categoryIn" where arg. + $variables = [ + 'calculateRatingCounts' => true, + 'where' => [ + 'categoryIn' => [ 'clothing' ], + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_KIND', + 'relation' => "AND", + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Query for products with the "shoes" tag and confirm correct count. + */ + $variables = [ + 'calculateRatingCounts' => true, + 'where' => [ + 'tagIdIn' => [ $shoes_tag_id ], + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_KIND', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_KIND' ), + $this + ->not() + ->expectedNode( 'terms', [ $this->expectedField( 'node.slug', 'normal' ) ] ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'special' ), + $this->expectedField( 'count', 5 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + ], + 0 + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + // Test again with the "tagIn" where arg. + $variables = [ + 'calculateRatingCounts' => true, + 'where' => [ + 'tagIn' => [ 'shoes' ], + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_KIND', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( $response, $expected ); + + /** + * Query for products with the "shoes" tag and confirm correct count. + */ + $variables = [ + 'calculateRatingCounts' => true, + 'where' => [ + 'categoryIn' => [ 'clothing' ], + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'ids' => $normal_term_id, + 'operator' => 'IN', + ], + ] + ], + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_KIND', + 'relation' => "AND", + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_KIND' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'normal' ), + $this->expectedField( 'count', 20 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ] + ), + $this + ->not() + ->expectedNode( 'terms', [ $this->expectedField( 'node.slug', 'special' ) ] ), + ], + 0 + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCollectionStatsQueryWithOrTaxQueries() { + // Create product attributes. + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special' ], 'Product type' ); + $pattern_attribute = $this->factory->product->createAttribute( 'pattern', [ 'polka-dot', 'striped' ], 'Product pattern' ); + + $normal_term_id = get_term_by( 'slug', 'normal', 'pa_kind' )->term_id; + $special_term_id = get_term_by( 'slug', 'special', 'pa_kind' )->term_id; + $kind_attribute_normal_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $normal_term_id ] + ); + $kind_attribute_special_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $special_term_id ] + ); + + $polka_dot_term_id = get_term_by( 'slug', 'polka-dot', 'pa_pattern' )->term_id; + $striped_term_id = get_term_by( 'slug', 'striped', 'pa_pattern' )->term_id; + $pattern_attribute_polka_dot_only = $this->factory->product->createAttributeObject( + $pattern_attribute['attribute_id'], + $pattern_attribute['attribute_taxonomy'], + [ $polka_dot_term_id ] + ); + $pattern_attribute_striped_only = $this->factory->product->createAttributeObject( + $pattern_attribute['attribute_id'], + $pattern_attribute['attribute_taxonomy'], + [ $striped_term_id ] + ); + + // Create products. + $this->factory->product->createManySimple( + 3, + [ + 'attributes' => [ $kind_attribute_normal_only ], + 'default_attributes' => [ 'pa_kind' => 'normal' ], + ] + ); + $this->factory->product->createManySimple( + 7, + [ + 'attributes' => [ $kind_attribute_special_only ], + 'default_attributes' => [ 'pa_kind' => 'special' ], + ] + ); + $this->factory->product->createManySimple( + 4, + [ + 'attributes' => [ $pattern_attribute_polka_dot_only ], + 'default_attributes' => [ 'pa_pattern' => 'polka-dot' ], + ] + ); + $this->factory->product->createManySimple( + 6, + [ + 'attributes' => [ $pattern_attribute_striped_only ], + 'default_attributes' => [ 'pa_pattern' => 'striped' ], + ] + ); + $this->factory->product->createManySimple( + 2, + [ + 'attributes' => [ + $kind_attribute_normal_only, + $pattern_attribute_polka_dot_only, + ], + 'default_attributes' => [ 'pa_kind' => 'normal', 'pa_pattern' => 'polka-dot' ], + ] + ); + $this->factory->product->createManySimple( + 8, + [ + 'attributes' => [ + $kind_attribute_special_only, + $pattern_attribute_striped_only, + ], + 'default_attributes' => [ 'pa_kind' => 'special', 'pa_pattern' => 'striped' ], + ] + ); + + $query = ' + query ($where: CollectionStatsWhereArgs, $taxonomies: [CollectionStatsQueryInput]) { + collectionStats( + calculateRatingCounts: true + taxonomies: $taxonomies + where: $where + ) { + attributeCounts { + name + slug + label + terms { + node { slug } + termId + count + } + } + } + } + '; + + $variables = [ + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_KIND', + 'relation' => 'OR', + ], + [ + 'taxonomy' => 'PA_PATTERN', + 'relation' => 'OR', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_KIND' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'normal' ), + $this->expectedField( 'count', 5 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 0 + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'special' ), + $this->expectedField( 'count', 15 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 1 + ), + ], + ), + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_PATTERN' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'polka-dot' ), + $this->expectedField( 'count', 6 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 0 + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'striped' ), + $this->expectedField( 'count', 14 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 1 + ), + ], + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCollectionStatsQueryWithAndTaxQueries() { + // Create product attributes. + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special' ], 'Product type' ); + $pattern_attribute = $this->factory->product->createAttribute( 'pattern', [ 'polka-dot', 'striped' ], 'Product pattern' ); + + $normal_term_id = get_term_by( 'slug', 'normal', 'pa_kind' )->term_id; + $special_term_id = get_term_by( 'slug', 'special', 'pa_kind' )->term_id; + $kind_attribute_normal_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $normal_term_id ] + ); + $kind_attribute_special_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $special_term_id ] + ); + + $polka_dot_term_id = get_term_by( 'slug', 'polka-dot', 'pa_pattern' )->term_id; + $striped_term_id = get_term_by( 'slug', 'striped', 'pa_pattern' )->term_id; + $pattern_attribute_polka_dot_only = $this->factory->product->createAttributeObject( + $pattern_attribute['attribute_id'], + $pattern_attribute['attribute_taxonomy'], + [ $polka_dot_term_id ] + ); + $pattern_attribute_striped_only = $this->factory->product->createAttributeObject( + $pattern_attribute['attribute_id'], + $pattern_attribute['attribute_taxonomy'], + [ $striped_term_id ] + ); + + // Create taxonomies. + $clothing_category_id = $this->factory->product->createProductCategory( 'clothing' ); + + // Create products. + $this->factory->product->createManySimple( + 3, + [ + 'category_ids' => [ $clothing_category_id ], + 'attributes' => [ $kind_attribute_normal_only ], + 'default_attributes' => [ 'pa_kind' => 'normal' ], + ] + ); + $this->factory->product->createManySimple( + 7, + [ + 'attributes' => [ $kind_attribute_special_only ], + 'default_attributes' => [ 'pa_kind' => 'special' ], + ] + ); + $this->factory->product->createManySimple( + 4, + [ + 'category_ids' => [ $clothing_category_id ], + 'attributes' => [ $pattern_attribute_polka_dot_only ], + 'default_attributes' => [ 'pa_pattern' => 'polka-dot' ], + ] + ); + $this->factory->product->createManySimple( + 6, + [ + 'attributes' => [ $pattern_attribute_striped_only ], + 'default_attributes' => [ 'pa_pattern' => 'striped' ], + ] + ); + $this->factory->product->createManySimple( + 2, + [ + 'attributes' => [ + $kind_attribute_normal_only, + $pattern_attribute_polka_dot_only, + ], + 'default_attributes' => [ 'pa_kind' => 'normal', 'pa_pattern' => 'polka-dot' ], + ] + ); + $this->factory->product->createManySimple( + 8, + [ + 'category_ids' => [ $clothing_category_id ], + 'attributes' => [ + $kind_attribute_special_only, + $pattern_attribute_striped_only, + ], + 'default_attributes' => [ 'pa_kind' => 'special', 'pa_pattern' => 'striped' ], + ] + ); + + $query = ' + query ($where: CollectionStatsWhereArgs, $taxonomies: [CollectionStatsQueryInput]) { + collectionStats( + calculateRatingCounts: true + taxonomies: $taxonomies + where: $where + ) { + attributeCounts { + name + slug + label + terms { + node { slug } + termId + count + } + } + } + } + '; + + $variables = [ + 'where' => [ + 'categoryIn' => [ 'clothing' ], + ], + 'taxonomies' => [ + [ + 'taxonomy' => 'PA_PATTERN', + 'relation' => 'OR', + ], + [ + 'taxonomy' => 'PA_KIND', + 'relation' => 'AND', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_KIND' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'normal' ), + $this->expectedField( 'count', 3 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 0 + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'special' ), + $this->expectedField( 'count', 8 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 1 + ), + ], + ), + $this->expectedNode( + 'collectionStats.attributeCounts', + [ + $this->expectedField( 'slug', 'PA_PATTERN' ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'polka-dot' ), + $this->expectedField( 'count', 4 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 0 + ), + $this->expectedNode( + 'terms', + [ + $this->expectedField( 'node.slug', 'striped' ), + $this->expectedField( 'count', 8 ), + $this->expectedField( 'termId', self::NOT_FALSY ), + ], + 1 + ), + ], + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } } diff --git a/tests/wpunit/ConnectionPaginationTest.php b/tests/wpunit/ConnectionPaginationTest.php index 9de0623e7..0c055c319 100644 --- a/tests/wpunit/ConnectionPaginationTest.php +++ b/tests/wpunit/ConnectionPaginationTest.php @@ -131,11 +131,11 @@ static function ( $key_a, $key_b ) { public function testProductsPagination() { $products = [ - $this->factory->product->createSimple(), - $this->factory->product->createSimple(), - $this->factory->product->createSimple(), - $this->factory->product->createSimple(), - $this->factory->product->createSimple(), + $this->factory->product->createSimple( [ 'menu_order' => 0 ] ), + $this->factory->product->createSimple( [ 'menu_order' => 1 ] ), + $this->factory->product->createSimple( [ 'menu_order' => 2 ] ), + $this->factory->product->createSimple( [ 'menu_order' => 3 ] ), + $this->factory->product->createSimple( [ 'menu_order' => 4 ] ), ]; usort( diff --git a/tests/wpunit/CustomerQueriesTest.php b/tests/wpunit/CustomerQueriesTest.php index 7ba2a887e..5ea005629 100644 --- a/tests/wpunit/CustomerQueriesTest.php +++ b/tests/wpunit/CustomerQueriesTest.php @@ -70,13 +70,13 @@ public function expectedCustomerData( $id ) { 'jwtAuthToken', ! is_wp_error( \WPGraphQL\JWT_Authentication\Auth::get_token( $wp_user ) ) ? \WPGraphQL\JWT_Authentication\Auth::get_token( $wp_user ) - : null + : self::IS_NULL ), $this->expectedField( 'jwtRefreshToken', ! is_wp_error( \WPGraphQL\JWT_Authentication\Auth::get_refresh_token( $wp_user ) ) ? \WPGraphQL\JWT_Authentication\Auth::get_refresh_token( $wp_user ) - : null + : self::IS_NULL ), ] ), diff --git a/tests/wpunit/ProductQueriesTest.php b/tests/wpunit/ProductQueriesTest.php index f67ed19a3..606fcdf2b 100644 --- a/tests/wpunit/ProductQueriesTest.php +++ b/tests/wpunit/ProductQueriesTest.php @@ -464,6 +464,16 @@ public function testProductsQueryAndWhereArgs() { ] ), $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 = ' @@ -485,6 +495,7 @@ public function testProductsQueryAndWhereArgs() { $taxonomyFilter: ProductTaxonomyInput $include: [Int] $exclude: [Int] + $stockStatus: [StockStatusEnum] ) { products( where: { slugIn: $slugIn, @@ -504,6 +515,7 @@ public function testProductsQueryAndWhereArgs() { taxonomyFilter: $taxonomyFilter include: $include exclude: $exclude + stockStatus: $stockStatus } ) { nodes { id @@ -511,6 +523,9 @@ public function testProductsQueryAndWhereArgs() { databaseId price } + ... on InventoriedProduct { + stockStatus + } } } } @@ -679,7 +694,7 @@ static function ( $node, $index ) use ( $product_ids ) { $this->expectedNode( 'products.nodes', [ $this->expectedField( 'id', $this->toRelayId( 'product', $product_ids[1] ) ) ], - 3 + 4 ), ]; @@ -869,6 +884,32 @@ static function ( $node, $index ) use ( $product_ids, $category_4, $category_3 ) ), ]; $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() { @@ -1460,4 +1501,200 @@ public function testProductQueryWithInterfaces() { $this->assertQuerySuccessful( $response, $expected ); } + + public function testProductsQueryWithAttributesFilter() { + // Create product attributes. + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'special', 'normal' ], 'Product type' ); + $normal_term_id = get_term_by( 'slug', 'normal', 'pa_kind' )->term_id; + $special_term_id = get_term_by( 'slug', 'special', 'pa_kind' )->term_id; + + // Create attribute objects. + $kind_attribute_normal_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $normal_term_id ] + ); + + $kind_attribute_special_only = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $special_term_id ] + ); + + $kind_attribute_both = $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + [ $normal_term_id, $special_term_id ] + ); + + // Create products. + $normal_product_id = $this->factory->product->createSimple( + [ + 'attributes' => [ $kind_attribute_normal_only ], + 'default_attributes' => [ 'pa_kind' => 'normal' ], + ] + ); + $special_product_id = $this->factory->product->createSimple( + [ + 'attributes' => [ $kind_attribute_special_only ], + 'default_attributes' => [ 'pa_kind' => 'special' ], + ] + ); + $both_product_id = $this->factory->product->createSimple( + [ + 'attributes' => [ $kind_attribute_both ], + ] + ); + + // Create query. + $query = ' + query( $where: RootQueryToProductUnionConnectionWhereArgs ) { + products( where: $where ) { + nodes { + ... on SimpleProduct { + id + } + } + } + } + '; + + $variables = [ + 'where' => [ + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'normal' ], + ], + ], + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ 'id' => $this->toRelayId( 'product', $normal_product_id ) ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ + 'where' => [ + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'special' ], + ], + ], + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ 'id' => $this->toRelayId( 'product', $special_product_id ) ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ + 'where' => [ + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'normal', 'special' ], + ], + ], + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ 'id' => $this->toRelayId( 'product', $both_product_id ) ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ + 'where' => [ + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'normal', 'special' ], + 'operator' => 'NOT_IN', + ], + ], + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( + 'products.nodes', + self::IS_FALSY + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ + 'where' => [ + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'normal' ], + 'operator' => 'NOT_IN', + ], + ] + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ 'id' => $this->toRelayId( 'product', $special_product_id ) ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + $variables = [ + 'where' => [ + 'attributes' => [ + 'queries' => [ + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'normal' ], + ], + [ + 'taxonomy' => 'PA_KIND', + 'terms' => [ 'special' ], + ], + ], + 'relation' => 'AND', + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedNode( + 'products.nodes', + [ 'id' => $this->toRelayId( 'product', $both_product_id ) ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } } diff --git a/tests/wpunit/ProductVariationQueriesTest.php b/tests/wpunit/ProductVariationQueriesTest.php index ad4fdceb9..e7fdda8bf 100644 --- a/tests/wpunit/ProductVariationQueriesTest.php +++ b/tests/wpunit/ProductVariationQueriesTest.php @@ -202,6 +202,7 @@ public function testVariationsQueryAndWhereArgs() { } ) { nodes { id + price } } } diff --git a/tests/wpunit/VariationAttributeQueriesTest.php b/tests/wpunit/VariationAttributeQueriesTest.php index 07e9e8740..1e5808bdd 100644 --- a/tests/wpunit/VariationAttributeQueriesTest.php +++ b/tests/wpunit/VariationAttributeQueriesTest.php @@ -165,7 +165,6 @@ public function testSimpleProductToVariationAttributeQuery() { ]; $attributes = array_map( static function ( $data, $index ) { - \codecept_debug( $data ); $attribute = new \WC_Product_Attribute(); $attribute->set_id( $data['attribute_id'] ); $attribute->set_name( $data['attribute_taxonomy'] );