From 5ca8f4fec9b402c2a4fbcb1d66036d5291d65b49 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 7 May 2024 13:56:02 -0400 Subject: [PATCH 1/3] devops: Product and product attribute mutation tests generated --- composer.json | 1 + composer.lock | 1830 ++++++++++++++++- .../wpunit/ProductAttributeMutationsTest.php | 15 + tests/wpunit/ProductMutationsTest.php | 15 + 4 files changed, 1816 insertions(+), 45 deletions(-) create mode 100644 tests/wpunit/ProductAttributeMutationsTest.php create mode 100644 tests/wpunit/ProductMutationsTest.php diff --git a/composer.json b/composer.json index dde66048..75c8ba11 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "phpstan/extension-installer": "^1.3", "phpstan/phpdoc-parser": "^1.22.0", "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6", "szepeviktor/phpstan-wordpress": "^1.3" }, "config": { diff --git a/composer.lock b/composer.lock index d779fc92..886ba802 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": "ad8868feb70f92f094704f97620ec76b", "packages": [ { "name": "firebase/php-jwt", @@ -313,6 +313,311 @@ }, "time": "2023-01-05T11:28:13+00:00" }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + }, + "time": "2024-03-05T20:51:40+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, { "name": "php-stubs/woocommerce-stubs", "version": "v7.9.0", @@ -866,86 +1171,1471 @@ "time": "2023-11-05T12:57:57+00:00" }, { - "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.17", + "name": "phpunit/php-code-coverage", + "version": "9.2.31", "source": { "type": "git", - "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", - "sirbrillig/phpcs-import-detection": "^1.1", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" + "phpunit/phpunit": "^9.3" }, - "type": "phpcodesniffer-standard", - "autoload": { - "psr-4": { - "VariableAnalysis\\": "VariableAnalysis/" + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-3-Clause" ], "authors": [ { - "name": "Sam Graham", - "email": "php-codesniffer-variableanalysis@illusori.co.uk" - }, - { - "name": "Payton Swick", - "email": "payton@foolord.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "A PHPCS sniff to detect problems with variables.", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "phpcs", - "static analysis" + "coverage", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", - "source": "https://github.com/sirbrillig/phpcs-variable-analysis", - "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, - "time": "2023-08-05T23:46:11+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:37:42+00:00" }, { - "name": "slevomat/coding-standard", - "version": "8.14.1", + "name": "phpunit/php-file-iterator", + "version": "3.0.6", "source": { "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "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" + "php": ">=7.3" }, "require-dev": { - "phing/phing": "2.17.4", - "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-04-05T04:35:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.11.17", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", + "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", + "sirbrillig/phpcs-import-detection": "^1.1", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], + "support": { + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "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" + }, + { + "name": "slevomat/coding-standard", + "version": "8.14.1", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "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" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", "phpstan/phpstan": "1.10.37", "phpstan/phpstan-deprecation-rules": "1.1.4", "phpstan/phpstan-phpunit": "1.3.14", @@ -1183,6 +2873,56 @@ }, "time": "2023-10-16T17:23:56+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.0.1", diff --git a/tests/wpunit/ProductAttributeMutationsTest.php b/tests/wpunit/ProductAttributeMutationsTest.php new file mode 100644 index 00000000..71877f55 --- /dev/null +++ b/tests/wpunit/ProductAttributeMutationsTest.php @@ -0,0 +1,15 @@ +markTestIncomplete(); + } + + public function testUpdateProductAttribute() { + $this->markTestIncomplete(); + } + + public function testDeleteProductAttribute() { + $this->markTestIncomplete(); + } +} \ No newline at end of file diff --git a/tests/wpunit/ProductMutationsTest.php b/tests/wpunit/ProductMutationsTest.php new file mode 100644 index 00000000..d5be93aa --- /dev/null +++ b/tests/wpunit/ProductMutationsTest.php @@ -0,0 +1,15 @@ +markTestIncomplete(); + } + + public function testUpdateProduct() { + $this->markTestIncomplete(); + } + + public function testDeleteProduct() { + $this->markTestIncomplete(); + } +} \ No newline at end of file From ca7a3f995915541fe37c85eabbdccd00cdb0671a Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 8 May 2024 04:15:29 -0400 Subject: [PATCH 2/3] feat: Product Mutations implemented and tested --- composer.json | 1 - composer.lock | 1826 +---------------- includes/class-core-schema-filters.php | 1 + includes/class-type-registry.php | 46 +- includes/class-wp-graphql-woocommerce.php | 23 +- .../data/mutation/class-product-mutation.php | 329 +++ .../class-product-attribute-create.php | 128 ++ .../class-product-attribute-delete.php | 98 + .../class-product-attribute-term-create.php | 149 ++ .../class-product-attribute-term-delete.php | 119 ++ .../class-product-attribute-term-update.php | 74 + .../class-product-attribute-update.php | 117 ++ includes/mutation/class-product-create.php | 502 +++++ includes/mutation/class-product-delete.php | 167 ++ includes/mutation/class-product-update.php | 71 + .../class-product-variation-create.php | 336 +++ .../class-product-variation-delete.php | 145 ++ .../class-product-variation-update.php | 71 + .../input/class-product-attribute-input.php | 3 + .../input/class-product-attributes-input.php | 54 + .../input/class-product-dimensions-input.php | 42 + .../input/class-product-download-input.php | 42 + .../type/input/class-product-image-input.php | 46 + .../class-product-attribute-object-type.php | 75 + ...ass-product-attribute-term-object-type.php | 75 + tests/_support/Factory/ProductFactory.php | 28 + .../_support/TestCase/WooGraphQLTestCase.php | 1 + .../wpunit/ProductAttributeMutationsTest.php | 171 +- .../ProductAttributeTermMutationsTest.php | 133 ++ tests/wpunit/ProductMutationsTest.php | 523 ++++- .../wpunit/ProductVariationMutationsTest.php | 175 ++ 31 files changed, 3764 insertions(+), 1807 deletions(-) create mode 100644 includes/data/mutation/class-product-mutation.php create mode 100644 includes/mutation/class-product-attribute-create.php create mode 100644 includes/mutation/class-product-attribute-delete.php create mode 100644 includes/mutation/class-product-attribute-term-create.php create mode 100644 includes/mutation/class-product-attribute-term-delete.php create mode 100644 includes/mutation/class-product-attribute-term-update.php create mode 100644 includes/mutation/class-product-attribute-update.php create mode 100644 includes/mutation/class-product-create.php create mode 100644 includes/mutation/class-product-delete.php create mode 100644 includes/mutation/class-product-update.php create mode 100644 includes/mutation/class-product-variation-create.php create mode 100644 includes/mutation/class-product-variation-delete.php create mode 100644 includes/mutation/class-product-variation-update.php create mode 100644 includes/type/input/class-product-attributes-input.php create mode 100644 includes/type/input/class-product-dimensions-input.php create mode 100644 includes/type/input/class-product-download-input.php create mode 100644 includes/type/input/class-product-image-input.php create mode 100644 includes/type/object/class-product-attribute-object-type.php create mode 100644 includes/type/object/class-product-attribute-term-object-type.php create mode 100644 tests/wpunit/ProductAttributeTermMutationsTest.php create mode 100644 tests/wpunit/ProductVariationMutationsTest.php diff --git a/composer.json b/composer.json index 75c8ba11..dde66048 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,6 @@ "phpstan/extension-installer": "^1.3", "phpstan/phpdoc-parser": "^1.22.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6", "szepeviktor/phpstan-wordpress": "^1.3" }, "config": { diff --git a/composer.lock b/composer.lock index 886ba802..d779fc92 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": "ad8868feb70f92f094704f97620ec76b", + "content-hash": "0a63256d1a0b55f5be349b050cf48c59", "packages": [ { "name": "firebase/php-jwt", @@ -313,311 +313,6 @@ }, "time": "2023-01-05T11:28:13+00:00" }, - { - "name": "doctrine/instantiator", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:23:10+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.11.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2023-03-08T13:26:56+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v5.0.2", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" - }, - "time": "2024-03-05T20:51:40+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, { "name": "php-stubs/woocommerce-stubs", "version": "v7.9.0", @@ -1171,1467 +866,82 @@ "time": "2023-11-05T12:57:57+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.11.17", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", + "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" }, "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.2-dev" - } + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", + "sirbrillig/phpcs-import-detection": "^1.1", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" }, + "type": "phpcodesniffer-standard", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "BSD-2-Clause" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "A PHPCS sniff to detect problems with variables.", "keywords": [ - "coverage", - "testing", - "xunit" + "phpcs", + "static analysis" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2023-08-05T23:46:11+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "name": "slevomat/coding-standard", + "version": "8.14.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2021-12-02T12:48:52+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "5.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:16:10+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "9.6.19", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.3.1 || ^2", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", - "sebastian/version": "^3.0.2" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.6-dev" - } - }, - "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" - } - ], - "time": "2024-04-05T04:35:58+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" - }, - { - "name": "sebastian/comparator", - "version": "4.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-14T12:41:17+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-22T06:19:30+00:00" - }, - { - "name": "sebastian/diff", - "version": "4.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:30:58+00:00" - }, - { - "name": "sebastian/environment", - "version": "5.1.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:03:51+00:00" - }, - { - "name": "sebastian/exporter", - "version": "4.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:33:00+00:00" - }, - { - "name": "sebastian/global-state", - "version": "5.0.7", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:35:11+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-22T06:20:34+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:12:34+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:14:26+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "4.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:07:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-14T16:00:52+00:00" - }, - { - "name": "sebastian/type", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T06:13:03+00:00" - }, - { - "name": "sebastian/version", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" - }, - { - "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.17", - "source": { - "type": "git", - "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/3b71162a6bf0cde2bff1752e40a1788d8273d049", - "reference": "3b71162a6bf0cde2bff1752e40a1788d8273d049", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", - "sirbrillig/phpcs-import-detection": "^1.1", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" - }, - "type": "phpcodesniffer-standard", - "autoload": { - "psr-4": { - "VariableAnalysis\\": "VariableAnalysis/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Sam Graham", - "email": "php-codesniffer-variableanalysis@illusori.co.uk" - }, - { - "name": "Payton Swick", - "email": "payton@foolord.com" - } - ], - "description": "A PHPCS sniff to detect problems with variables.", - "keywords": [ - "phpcs", - "static analysis" - ], - "support": { - "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", - "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" - }, - { - "name": "slevomat/coding-standard", - "version": "8.14.1", - "source": { - "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", - "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" + "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" }, "require-dev": { "phing/phing": "2.17.4", @@ -2873,56 +1183,6 @@ }, "time": "2023-10-16T17:23:56+00:00" }, - { - "name": "theseer/tokenizer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:36:25+00:00" - }, { "name": "wp-coding-standards/wpcs", "version": "3.0.1", diff --git a/includes/class-core-schema-filters.php b/includes/class-core-schema-filters.php index 4e621ece..38b8af2d 100644 --- a/includes/class-core-schema-filters.php +++ b/includes/class-core-schema-filters.php @@ -129,6 +129,7 @@ public static function register_post_types( $args, $post_type ) { $args['graphql_interfaces'] = [ 'ContentNode' ]; $args['graphql_register_root_field'] = false; $args['graphql_register_root_connection'] = false; + $args['graphql_exclude_mutations'] = [ 'create', 'delete', 'update' ]; $args['graphql_resolve_type'] = static function ( $value ) { $type_registry = \WPGraphQL::get_type_registry(); $possible_types = WooGraphQL::get_enabled_product_types(); diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index c679b54a..c96d08ee 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -66,6 +66,10 @@ 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_Attributes_Input::register(); + Type\WPInputObject\Product_Dimensions_Input::register(); + Type\WPInputObject\Product_Download_Input::register(); + Type\WPInputObject\Product_Image_Input::register(); /** * Interfaces. @@ -111,6 +115,8 @@ public function init() { Type\WPObject\Payment_Token_Types::register(); Type\WPObject\Country_State_Type::register(); Type\WPObject\Collection_Stats_Type::register(); + Type\WPObject\Product_Attribute_Object_Type::register(); + Type\WPObject\Product_Attribute_Term_Object_Type::register(); /** * Object fields. @@ -148,32 +154,44 @@ public function init() { /** * Mutations. */ - Mutation\Customer_Register::register_mutation(); - Mutation\Customer_Update::register_mutation(); + Mutation\Cart_Add_Fee::register_mutation(); Mutation\Cart_Add_Item::register_mutation(); Mutation\Cart_Add_Items::register_mutation(); - Mutation\Cart_Update_Item_Quantities::register_mutation(); - Mutation\Cart_Remove_Items::register_mutation(); - Mutation\Cart_Restore_Items::register_mutation(); - Mutation\Cart_Empty::register_mutation(); Mutation\Cart_Apply_Coupon::register_mutation(); + Mutation\Cart_Empty::register_mutation(); + Mutation\Cart_Fill::register_mutation(); + Mutation\Cart_Remove_Items::register_mutation(); Mutation\Cart_Remove_Coupons::register_mutation(); - Mutation\Cart_Add_Fee::register_mutation(); + Mutation\Cart_Restore_Items::register_mutation(); + Mutation\Cart_Update_Item_Quantities::register_mutation(); Mutation\Cart_Update_Shipping_Method::register_mutation(); - Mutation\Cart_Fill::register_mutation(); + Mutation\Checkout::register_mutation(); + Mutation\Coupon_Create::register_mutation(); + Mutation\Coupon_Update::register_mutation(); + Mutation\Coupon_Delete::register_mutation(); + Mutation\Customer_Register::register_mutation(); + Mutation\Customer_Update::register_mutation(); Mutation\Order_Create::register_mutation(); Mutation\Order_Update::register_mutation(); Mutation\Order_Delete::register_mutation(); Mutation\Order_Delete_Items::register_mutation(); - Mutation\Checkout::register_mutation(); + Mutation\Payment_Method_Delete::register_mutation(); + Mutation\Payment_Method_Set_Default::register_mutation(); + Mutation\Product_Attribute_Create::register_mutation(); + Mutation\Product_Attribute_Update::register_mutation(); + Mutation\Product_Attribute_Delete::register_mutation(); + Mutation\Product_Attribute_Term_Create::register_mutation(); + Mutation\Product_Attribute_Term_Update::register_mutation(); + Mutation\Product_Attribute_Term_Delete::register_mutation(); + Mutation\Product_Create::register_mutation(); + Mutation\Product_Update::register_mutation(); + Mutation\Product_Delete::register_mutation(); + Mutation\Product_Variation_Create::register_mutation(); + Mutation\Product_Variation_Update::register_mutation(); + Mutation\Product_Variation_Delete::register_mutation(); Mutation\Review_Write::register_mutation(); Mutation\Review_Update::register_mutation(); Mutation\Review_Delete_Restore::register_mutation(); - Mutation\Coupon_Create::register_mutation(); - Mutation\Coupon_Update::register_mutation(); - Mutation\Coupon_Delete::register_mutation(); - Mutation\Payment_Method_Delete::register_mutation(); - Mutation\Payment_Method_Set_Default::register_mutation(); Mutation\Update_Session::register_mutation(); } } diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index bd99096c..01c2e9b0 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -205,6 +205,7 @@ private function includes() { require $include_directory_path . 'data/mutation/class-coupon-mutation.php'; require $include_directory_path . 'data/mutation/class-customer-mutation.php'; require $include_directory_path . 'data/mutation/class-order-mutation.php'; + require $include_directory_path . 'data/mutation/class-product-mutation.php'; // Include factory class file. require $include_directory_path . 'data/class-factory.php'; @@ -282,6 +283,8 @@ private function includes() { require $include_directory_path . 'type/object/class-payment-token-types.php'; require $include_directory_path . 'type/object/class-country-state-type.php'; require $include_directory_path . 'type/object/class-collection-stats-type.php'; + require $include_directory_path . 'type/object/class-product-attribute-object-type.php'; + require $include_directory_path . 'type/object/class-product-attribute-term-object-type.php'; // Include input type class files. require $include_directory_path . 'type/input/class-cart-item-input.php'; @@ -300,6 +303,10 @@ 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-attributes-input.php'; + require $include_directory_path . 'type/input/class-product-dimensions-input.php'; + require $include_directory_path . 'type/input/class-product-download-input.php'; + require $include_directory_path . 'type/input/class-product-image-input.php'; // Include mutation type class files. require $include_directory_path . 'mutation/class-cart-add-fee.php'; @@ -323,11 +330,23 @@ private function includes() { require $include_directory_path . 'mutation/class-order-delete-items.php'; require $include_directory_path . 'mutation/class-order-delete.php'; require $include_directory_path . 'mutation/class-order-update.php'; + require $include_directory_path . 'mutation/class-payment-method-delete.php'; + require $include_directory_path . 'mutation/class-payment-method-set-default.php'; + require $include_directory_path . 'mutation/class-product-attribute-create.php'; + require $include_directory_path . 'mutation/class-product-attribute-delete.php'; + require $include_directory_path . 'mutation/class-product-attribute-term-create.php'; + require $include_directory_path . 'mutation/class-product-attribute-term-delete.php'; + require $include_directory_path . 'mutation/class-product-attribute-term-update.php'; + require $include_directory_path . 'mutation/class-product-attribute-update.php'; + require $include_directory_path . 'mutation/class-product-create.php'; + require $include_directory_path . 'mutation/class-product-delete.php'; + require $include_directory_path . 'mutation/class-product-update.php'; + require $include_directory_path . 'mutation/class-product-variation-create.php'; + require $include_directory_path . 'mutation/class-product-variation-delete.php'; + require $include_directory_path . 'mutation/class-product-variation-update.php'; require $include_directory_path . 'mutation/class-review-write.php'; require $include_directory_path . 'mutation/class-review-delete-restore.php'; require $include_directory_path . 'mutation/class-review-update.php'; - require $include_directory_path . 'mutation/class-payment-method-delete.php'; - require $include_directory_path . 'mutation/class-payment-method-set-default.php'; require $include_directory_path . 'mutation/class-update-session.php'; // Include connection class/function files. diff --git a/includes/data/mutation/class-product-mutation.php b/includes/data/mutation/class-product-mutation.php new file mode 100644 index 00000000..2dc1c0a7 --- /dev/null +++ b/includes/data/mutation/class-product-mutation.php @@ -0,0 +1,329 @@ +set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $input['weight'] ) ) { + $product->set_weight( $input['weight'] ); + } + + if ( isset( $input['dimensions']['height'] ) ) { + $product->set_height( $input['dimensions']['height'] ); + } + + if ( isset( $input['dimensions']['width'] ) ) { + $product->set_width( $input['dimensions']['width'] ); + } + + if ( isset( $input['dimensions']['length'] ) ) { + $product->set_length( $input['dimensions']['length'] ); + } + } + + if ( isset( $input['shippingClass'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $input['shippingClass'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Prepare product attribute + * + * @param array $attribute Product attribute data. + * + * @return \WC_Product_Attribute|null + */ + public static function prepare_attribute( $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + return null; + } + + if ( $attribute_id ) { + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $options ) ) { + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = []; + } + + if ( ! empty( $values ) ) { + $attribute_object = new \WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + + return $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + + $attribute_object = new \WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + + return $attribute_object; + } + + return null; + } + + /** + * Save product attributes + * + * @param WC_Data $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * + * @return \WC_Data + */ + public static function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + } + + /** + * Save product downloadable files + * + * @param WC_Data $product Product instance. + * @param array $downloads Downloads data. + * + * @return \WC_Data + */ + public static function save_downloadable_files( $product, $downloads ) { + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save variable product default attributes + * + * @param \WC_Data $product Product object. + * @param array $input Mutation input. + * + * @return \WC_Data + */ + public static function save_default_attributes( $product, $input ) { + if ( ! empty( $input['defaultAttributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = []; + + foreach ( $input['defaultAttributes'] as $attribute ) { + $attribue_id = 0; + $attribute_name = ''; + + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['attributeName'] ) ) { + $attribute_name = sanitize_title( $attribute['attributeName'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = $sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Set product images + * + * @param WC_Product $product Product instance. + * @param array $images Images data. + * + * @return \WC_Product + */ + public static function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : []; + + if ( ! empty( $images ) ) { + $gallery_positions = []; + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = \wc_rest_upload_image_from_url( $image['src'] ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new UserError( $upload->get_error_message() ); + } else { + continue; + } + } + + $attachment_id = \wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new UserError( + sprintf( __( '#%s is an invalid image ID.', 'wp-graphql-woocommerce' ), $attachment_id ) + ); + } + + $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); + + // Set the image alt if present. + if ( ! empty( $image['altText'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + [ + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ] + ); + } + + // Set the image source if present, for future reference. + if ( ! empty( $image['src'] ) ) { + update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); + } + } + + // Sort images and get IDs in correct order. + asort( $gallery_positions ); + + // Get gallery in correct order. + $gallery = array_keys( $gallery_positions ); + + // Featured image is in position 0. + $image_id = array_shift( $gallery ); + + // Set images. + $product->set_image_id( $image_id ); + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( [] ); + } + + return $product; + } + + public static function get_attribute( $id ) { + global $wpdb; + + $attribute = $wpdb->get_row( + $wpdb->prepare( + " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", + $id + ) + ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new UserError( __( 'Invalid attribute ID.', 'wp-graphql-woocommerce' ) ); + } + + return $attribute; + } +} \ No newline at end of file diff --git a/includes/mutation/class-product-attribute-create.php b/includes/mutation/class-product-attribute-create.php new file mode 100644 index 00000000..a3d67faf --- /dev/null +++ b/includes/mutation/class-product-attribute-create.php @@ -0,0 +1,128 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Slug of the attribute.', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'String', + 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), + ], + 'orderBy' => [ + 'type' => 'String', + 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), + ], + 'hasArchives' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the attribute has archives.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'attribute' => [ + 'type' => 'ProductAttributeObject', + 'resolve' => static function ( $payload ) { + return $payload['attribute']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + global $wpdb; + + $id = \wc_create_attribute( + [ + 'name' => $input['name'], + 'slug' => \wc_sanitize_taxonomy_name( stripslashes( $input['slug'] ) ), + 'type' => ! empty( $input['type'] ) ? $input['type'] : 'select', + 'order_by' => ! empty( $input['orderBy'] ) ? $input['orderBy'] : 'menu_order', + 'has_archives' => true === $input['hasArchives'], + ] + ); + + // Checks for errors. + if ( is_wp_error( $id ) ) { + throw new UserError( $id->get_error_message() ); + } + + $attribute = Product_Mutation::get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + throw new UserError( $attribute->get_error_message() ); + } + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param array $input Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'graphql_woocommerce_insert_product_attribute', $attribute, $input, true ); + + + return [ 'attribute' => $attribute ]; + }; + } +} diff --git a/includes/mutation/class-product-attribute-delete.php b/includes/mutation/class-product-attribute-delete.php new file mode 100644 index 00000000..405b7e6a --- /dev/null +++ b/includes/mutation/class-product-attribute-delete.php @@ -0,0 +1,98 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'attribute' => [ + 'type' => 'ProductAttributeObject', + 'resolve' => static function ( $payload ) { + return $payload['attribute']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + $attribute = Product_Mutation::get_attribute( $input['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $deleted = \wc_delete_attribute( $attribute->attribute_id ); + + if ( false === $deleted ) { + throw new UserError( __( 'Failed to delete attribute.', 'wp-graphql-woocommerce' ) ); + } + + /** + * Fires after a single attribute is deleted via the REST API. + * + * @param stdObject $attribute The deleted attribute. + */ + do_action( 'graphql_woocommerce_delete_product_attribute', $attribute ); + + return [ 'attribute' => $attribute ]; + }; + } +} diff --git a/includes/mutation/class-product-attribute-term-create.php b/includes/mutation/class-product-attribute-term-create.php new file mode 100644 index 00000000..062103af --- /dev/null +++ b/includes/mutation/class-product-attribute-term-create.php @@ -0,0 +1,149 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ self::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'attributeId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the attribute to which the term belongs.', 'wp-graphql' ), + ], + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'The name of the term.', 'wp-graphql' ), + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'The slug of the term.', 'wp-graphql' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'The description of the term.', 'wp-graphql' ), + ], + 'menuOrder' => [ + 'type' => 'Int', + 'description' => __( 'The order of the term in the menu.', 'wp-graphql' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'term' => [ + 'type' => 'ProductAttributeTermObject', + 'resolve' => static function ( $payload ) { + return (object) $payload['term']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { + if ( ! $input['attributeId'] ) { + throw new UserError( __( 'An attributeId is required to create a new product attribute term.', 'wp-graphql' ) ); + } + + $taxonomy = \wc_attribute_taxonomy_name_by_id( $input['attributeId'] ); + $id = isset( $input['id'] ) ? $input['id'] : null; + $args = []; + + if ( ! empty( $input['description'] ) ) { + $args['description'] = $input['description']; + } + + if ( ! empty( $input['slug'] ) ) { + $args['slug'] = $input['slug']; + } + + if ( $id && ! empty( $input['name'] ) ) { + $args['name'] = $input['name']; + } + + if ( $id && ! empty( $args ) ) { + $term = wp_update_term( $id, $taxonomy, $args ); + } elseif ( $id && isset( $input['menuOrder'] ) ) { + $term = get_term( $id, $taxonomy ); + } elseif ( ! empty( $input['name'] ) ) { + $name = $input['name']; + $term = wp_insert_term( $name, $taxonomy, $args ); + } else { + $updating = 'updateProductAttributeTerm' === $info->fieldName; + throw new UserError( + $updating + ? __( 'A name is required to create a new product attribute term.', 'wp-graphql' ) + : __( 'A valid term "id" and changeable parameter are required to update a product attribute term.', 'wp-graphql' ) + ); + } + + if ( is_wp_error( $term ) ) { + throw new UserError( $term->get_error_message() ); + } + + $term = get_term( $term['term_id'], $taxonomy ); + + if ( isset( $input['menuOrder'] ) ) { + update_term_meta( $term->term_id, 'order_' .$taxonomy, $input['menuOrder'] ); + } + + $menu_order = get_term_meta( $term->term_id, 'order_' . $taxonomy, true ); + $data = [ + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'menu_order' => (int) $menu_order, + 'count' => (int) $term->count, + ]; + + return [ 'term' => $data ]; + } +} diff --git a/includes/mutation/class-product-attribute-term-delete.php b/includes/mutation/class-product-attribute-term-delete.php new file mode 100644 index 00000000..4b432c80 --- /dev/null +++ b/includes/mutation/class-product-attribute-term-delete.php @@ -0,0 +1,119 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'attributeId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the attribute to which the term belongs.', 'wp-graphql' ), + ], + 'id' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the term to update.', 'wp-graphql' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'term' => [ + 'type' => 'ProductAttributeTermObject', + 'resolve' => static function ( $payload ) { + return (object) $payload['term']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + if ( ! $input['attributeId'] ) { + throw new UserError( __( 'A valid attributeId is required to create a new product attribute term.', 'wp-graphql' ) ); + } + + $taxonomy = \wc_attribute_taxonomy_name_by_id( $input['attributeId'] ); + + if ( ! $input['id'] ) { + throw new UserError( __( 'A valid term ID is required to delete a product attribute term.', 'wp-graphql' ) ); + } + + $term = get_term( $input['id'], $taxonomy ); + if ( is_wp_error( $term ) ) { + throw new UserError( __( 'Invalid term ID.', 'wp-graphql' ) ); + } + + $data = [ + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'menuOrder' => $term->menu_order, + 'count' => $term->count, + ]; + + $retval = wp_delete_term( $term->term_id, $term->taxonomy ); + if ( ! $retval ) { + throw new UserError( __( 'Failed to delete term.', 'wp-graphql' ) ); + } + + /** + * Fires after a single term is deleted via the REST API. + * + * @param WP_Term $term The deleted term. + * @param array $input Mutation input. + */ + do_action( "graphql_woocommerce_delete_{$taxonomy}", $term, $input ); + + return [ 'term' => $data ]; + }; + } +} diff --git a/includes/mutation/class-product-attribute-term-update.php b/includes/mutation/class-product-attribute-term-update.php new file mode 100644 index 00000000..10920433 --- /dev/null +++ b/includes/mutation/class-product-attribute-term-update.php @@ -0,0 +1,74 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ Product_Attribute_Term_Create::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return array_merge( + Product_Attribute_Term_Create::get_input_fields(), + [ + 'id' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The ID of the term to update.', 'wp-graphql' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'The name of the term.', 'wp-graphql' ), + ], + ] + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'term' => [ + 'type' => 'ProductAttributeTermObject', + 'resolve' => static function ( $payload ) { + return (object) $payload['term']; + }, + ], + ]; + } +} diff --git a/includes/mutation/class-product-attribute-update.php b/includes/mutation/class-product-attribute-update.php new file mode 100644 index 00000000..060f23fc --- /dev/null +++ b/includes/mutation/class-product-attribute-update.php @@ -0,0 +1,117 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return array_merge( + [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + ], + Product_Attribute_Create::get_input_fields() + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'attribute' => [ + 'type' => 'ProductAttributeObject', + 'resolve' => static function ( $payload ) { + return $payload['attribute']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + global $wpdb; + + $id = (int) $input['id']; + $edited = \wc_update_attribute( + $id, + [ + 'name' => $input['name'], + 'slug' => \wc_sanitize_taxonomy_name( stripslashes( $input['slug'] ) ), + 'type' => ! empty( $input['type'] ) ? $input['type'] : 'select', + 'order_by' => ! empty( $input['orderBy'] ) ? $input['orderBy'] : 'menu_order', + 'has_archives' => true === $input['hasArchives'], + ] + ); + + // Checks for errors. + if ( is_wp_error( $id ) ) { + throw new UserError( $id->get_error_message() ); + } + + $attribute = Product_Mutation::get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return [ 'attribute' => $attribute ]; + } + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param array $input Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'graphql_woocommerce_insert_product_attribute', $attribute, $input, false ); + + + return [ 'attribute' => $attribute ]; + }; + } +} diff --git a/includes/mutation/class-product-create.php b/includes/mutation/class-product-create.php new file mode 100644 index 00000000..3768b79d --- /dev/null +++ b/includes/mutation/class-product-create.php @@ -0,0 +1,502 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ self::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Name of the product.', 'wp-graphql-woocommerce' ), + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Product slug.', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ProductTypesEnum', + 'description' => __( 'Type of the product.', 'wp-graphql-woocommerce' ), + ], + 'status' => [ + 'type' => 'PostStatusEnum', + 'description' => __( 'Status of the product.', 'wp-graphql-woocommerce' ), + ], + 'featured' => [ + 'type' => 'Boolean', + 'description' => __( 'Featured product.', 'wp-graphql-woocommerce' ), + ], + 'catalogVisibility' => [ + 'type' => 'CatalogVisibilityEnum', + 'description' => __( 'Catalog visibility.', 'wp-graphql-woocommerce' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Product description.', 'wp-graphql-woocommerce' ), + ], + 'shortDescription' => [ + 'type' => 'String', + 'description' => __( 'Product short description.', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Product SKU.', 'wp-graphql-woocommerce' ), + ], + 'regularPrice' => [ + 'type' => 'Float', + 'description' => __( 'Product regular price.', 'wp-graphql-woocommerce' ), + ], + 'salePrice' => [ + 'type' => 'Float', + 'description' => __( 'Product sale price.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleFrom' => [ + 'type' => 'String', + 'description' => __( 'Product sale start date.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleTo' => [ + 'type' => 'String', + 'description' => __( 'Product sale end date.', 'wp-graphql-woocommerce' ), + ], + 'virtual' => [ + 'type' => 'Boolean', + 'description' => __( 'Product virtual.', 'wp-graphql-woocommerce' ), + ], + 'downloadable' => [ + 'type' => 'Boolean', + 'description' => __( 'Product downloadable.', 'wp-graphql-woocommerce' ), + ], + 'downloads' => [ + 'type' => [ 'list_of' => 'ProductDownloadInput' ], + 'description' => __( 'Product downloads.', 'wp-graphql-woocommerce' ), + ], + 'downloadLimit' => [ + 'type' => 'Int', + 'description' => __( 'Product download limit.', 'wp-graphql-woocommerce' ), + ], + 'downloadExpiry' => [ + 'type' => 'Int', + 'description' => __( 'Number of days until download access expires.', 'wp-graphql-woocommerce' ), + ], + 'externalUrl' => [ + 'type' => 'String', + 'description' => __( 'Product external URL. (External products only)', 'wp-graphql-woocommerce' ), + ], + 'buttonText' => [ + 'type' => 'String', + 'description' => __( 'Product button text. (External products only)', 'wp-graphql-woocommerce' ), + ], + 'taxStatus' => [ + 'type' => 'TaxStatusEnum', + 'description' => __( 'Tax status.', 'wp-graphql-woocommerce' ), + ], + 'taxClass' => [ + 'type' => 'String', + 'description' => __( 'Tax class.', 'wp-graphql-woocommerce' ), + ], + 'manageStock' => [ + 'type' => 'Boolean', + 'description' => __( 'Manage stock.', 'wp-graphql-woocommerce' ), + ], + 'stockQuantity' => [ + 'type' => 'Int', + 'description' => __( 'Stock quantity.', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => 'StockStatusEnum', + 'description' => __( 'Stock status.', 'wp-graphql-woocommerce' ), + ], + 'backorders' => [ + 'type' => 'BackordersEnum', + 'description' => __( 'Backorders.', 'wp-graphql-woocommerce' ), + ], + 'soldIndividually' => [ + 'type' => 'Boolean', + 'description' => __( 'Sold individually.', 'wp-graphql-woocommerce' ), + ], + 'weight' => [ + 'type' => 'String', + 'description' => __( 'Product weight.', 'wp-graphql-woocommerce' ), + ], + 'dimensions' => [ + 'type' => 'ProductDimensionsInput', + 'description' => __( 'Product dimensions.', 'wp-graphql-woocommerce' ), + ], + 'shippingClass' => [ + 'type' => 'String', + 'description' => __( 'Shipping class.', 'wp-graphql-woocommerce' ), + ], + 'reviewsAllowed' => [ + 'type' => 'Boolean', + 'description' => __( 'Allow reviews. Default is true', 'wp-graphql-woocommerce' ), + ], + 'upsellIds' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Upsell product IDs.', 'wp-graphql-woocommerce' ), + ], + 'crossSellIds' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Cross-sell product IDs.', 'wp-graphql-woocommerce' ), + ], + 'parentId' => [ + 'type' => 'Int', + 'description' => __( 'Parent product ID.', 'wp-graphql-woocommerce' ), + ], + 'purchaseNote' => [ + 'type' => 'String', + 'description' => __( 'Purchase note.', 'wp-graphql-woocommerce' ), + ], + 'categories' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Product categories.', 'wp-graphql-woocommerce' ), + ], + 'tags' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Product tags.', 'wp-graphql-woocommerce' ), + ], + 'images' => [ + 'type' => [ 'list_of' => 'ProductImageInput' ], + 'description' => __( 'Product images.', 'wp-graphql-woocommerce' ), + ], + 'attributes' => [ + 'type' => [ 'list_of' => 'ProductAttributesInput' ], + 'description' => __( 'Product attributes.', 'wp-graphql-woocommerce' ), + ], + 'defaultAttributes' => [ + 'type' => [ 'list_of' => 'ProductAttributeInput' ], + 'description' => __( 'Product default attributes.', 'wp-graphql-woocommerce' ), + ], + 'variations' => [ + 'type' => [ 'list_of' => 'ProductVariationInput' ], + 'description' => __( 'Product variations.', 'wp-graphql-woocommerce' ), + ], + 'groupedProducts' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Grouped product IDs.', 'wp-graphql-woocommerce' ), + ], + 'menuOrder' => [ + 'type' => 'Int', + 'description' => __( 'Menu order.', 'wp-graphql-woocommerce' ), + ], + 'metaData' => [ + 'type' => [ 'list_of' => 'MetaDataInput' ], + 'description' => __( 'Meta data.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'product' => [ + 'type' => 'Product', + 'resolve' => static function ( $payload ) { + return new Product( $payload['id'] ); + }, + ], + 'productId' => [ + 'type' => 'Int', + 'resolve' => static function ( $payload ) { + return $payload['id']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { + $product_id = ! empty( $input['id'] ) ? $input['id'] : 0; + $type = ! empty( $input['type'] ) ? $input['type'] : 'simple'; + + if ( 0 !== $product_id ) { + $product = \wc_get_product( $product_id ); + if ( $product && ! wc_rest_check_post_permissions( 'product', 'edit', $product->get_id() ) ) { + throw new UserError( __( 'You do not have permission to edit this product', 'wp-graphql-woocommerce' ) ); + } + } else { + $classname = \WC_Product_Factory::get_classname_from_product_type( $type ); + if ( ! class_exists( $classname ) ) { + $classname = '\WC_Product_Simple'; + } + + $product = new $classname( $product_id ); + + $post_type_object = get_post_type_object( 'product' ); + if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) { + throw new UserError( __( 'You do not have permission to create products', 'wp-graphql-woocommerce' ) ); + } + } + + if ( ! empty( $input['name'] ) ) { + $product->set_name( wp_filter_post_kses( $input['name'] ) ); + } + + if ( ! empty( $input['description'] ) ) { + $product->set_description( wp_filter_post_kses( $input['description'] ) ); + } + + if ( ! empty( $input['shortDescription'] ) ) { + $product->set_short_description( wp_filter_post_kses( $input['shortDescription'] ) ); + } + + if ( ! empty( $input['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + if ( ! empty( $input['slug'] ) ) { + $product->set_slug( $input['slug'] ); + } + + if ( ! empty( $input['menuOrder'] ) ) { + $product->set_menu_order( $input['menuOrder'] ); + } + + if ( isset( $input['reviewsAllowed'] ) ) { + $product->set_reviews_allowed( $input['reviewsAllowed'] ); + } + + if ( isset( $input['virtual'] ) ) { + $product->set_virtual( $input['virtual'] ); + } + + if ( ! empty( $input['taxStatus'] ) ) { + $product->set_tax_status( $input['taxStatus'] ); + } + + if ( ! empty( $input['taxClass'] ) ) { + $product->set_tax_class( $input['taxClass'] ); + } + + if ( ! empty( $input['catalogVisibility'] ) ) { + $product->set_catalog_visibility( $input['catalogVisibility'] ); + } + + if ( ! empty( $input['purchaseNote'] ) ) { + $product->set_purchase_note( wp_filter_post_kses( $input['purchaseNote'] ) ); + } + + if ( isset( $input['featured'] ) ) { + $product->set_featured( $input['featured'] ); + } + + $product = Product_Mutation::save_product_shipping_data( $product, $input ); + + if ( ! empty( $input['sku'] ) ) { + $product->set_sku( wc_clean( $input['sku'] ) ); + } + + if ( ! empty( $input['attributes'] ) ) { + $attributes = []; + + foreach ( $input['attributes'] as $attribute ) { + $attribute_object = Product_Mutation::prepare_attribute( $attribute ); + if ( $attribute_object ) { + $attributes[] = $attribute_object; + } + } + + $product->set_attributes( $attributes ); + } + + if ( in_array( $type, [ 'variable', 'grouped' ], true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + if ( ! empty( $input['regularPrice'] ) ) { + $product->set_regular_price( $input['regularPrice'] ); + } + + if ( ! empty( $input['salePrice'] ) ) { + $product->set_sale_price( $input['salePrice'] ); + } + + if ( ! empty( $input['dateOnSaleFrom'] ) ) { + $product->set_date_on_sale_from( $input['dateOnSaleFrom'] ); + } + + if ( ! empty( $input['dateOnSaleTo'] ) ) { + $product->set_date_on_sale_to( $input['dateOnSaleTo'] ); + } + } + + if ( ! empty( $input['parentId'] ) ) { + $product->set_parent_id( $input['parentId'] ); + } + + if ( isset( $input['soldIndividually'] ) ) { + $product->set_sold_individually( $input['soldIndividually'] ); + } + + if ( isset( $input['stockStatus'] ) ) { + $stock_status = wc_clean( $input['stockStatus'] ); + } else { + $stock_status = $product->get_stock_status(); + } + + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + if ( isset( $input['manageStock'] ) ) { + $product->set_manage_stock( $input['manageStock'] ); + } + + + if ( isset( $input['backorders'] ) ) { + $product->set_backorders( $input['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $input['stockQuantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $input['stockQuantity'] ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + if ( ! empty( $input['upsellIds'] ) ) { + $product->set_upsell_ids( $input['upsellIds'] ); + } + + if ( ! empty( $input['crossSellIds'] ) ) { + $product->set_cross_sell_ids( $input['crossSellIds'] ); + } + + if ( ! empty( $input['categories'] ) ) { + $product = Product_Mutation::save_taxonomy_terms( $product, $input['categories'] ); + } + + if ( ! empty( $input['tags'] ) ) { + $product = Product_Mutation::save_taxonomy_terms( $product, $input['tags'], 'tag' ); + } + + if ( isset( $input['downloadable'] ) ) { + $product->set_downloadable( $input['downloadable'] ); + } + + if ( $product->get_downloadable() ) { + if ( ! empty( $input['downloads'] ) ) { + $product = Product_Mutation::save_downloadable_files( $product, $input['downloads'] ); + } + + if ( isset( $input['downloadLimit'] ) ) { + $product->set_download_limit( $input['downloadLimit'] ); + } + + if ( isset( $input['downloadExpiry'] ) ) { + $product->set_download_expiry( $input['downloadExpiry'] ); + } + } + + if ( $product->is_type( 'external' ) ) { + if ( ! empty( $input['externalUrl'] ) ) { + $product->set_product_url( $input['externalUrl'] ); + } + + if ( ! empty( $input['buttonText'] ) ) { + $product->set_button_text( $input['buttonText'] ); + } + } + + if ( $product->is_type( 'variable' ) ) { + $product = Product_Mutation::save_default_attributes( $product, $input ); + } + + if ( $product->is_type( 'grouped' ) && isset( $input['groupedProducts'] ) ) { + $product->set_children( $input['groupedProducts'] ); + } + + if ( ! empty( $input['images'] ) ) { + $product = Product_Mutation::set_product_images( $product, $input['images'] ); + } + + if ( ! empty( $input['metaData'] ) ) { + foreach( $input['metaData'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the GraphQL API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param array $input GraphQL input object. + * @param bool $creating If is creating a new object. + */ + $product = apply_filters( 'graphql_woocommerce_pre_insert_product_object', $product, $input, true ); + + $product_id = $product->save(); + + return [ 'id' => $product_id ]; + } +} diff --git a/includes/mutation/class-product-delete.php b/includes/mutation/class-product-delete.php new file mode 100644 index 00000000..5207c1c0 --- /dev/null +++ b/includes/mutation/class-product-delete.php @@ -0,0 +1,167 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + 'force' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'product' => [ + 'type' => 'Product', + 'resolve' => static function ( $payload ) { + return $payload['product']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + $product_id = $input['id']; + $force = isset( $input['force'] ) ? $input['force'] : false; + $object = new Product( $product_id ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + throw new UserError( __( 'Invalid product ID.', 'wp-graphql-woocommerce' ) ); + } + + if ( 'variation' === $object->get_type() ) { + throw new UserError( __( 'Variations cannot be deleted with this mutation. Use "deleteProductVariations" instead.', 'wp-graphql-woocommerce' ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( [ $object, 'get_status' ] ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param \WC_Product $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "graphql_woocommerce_product_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( 'product', 'delete', $object->get_id() ) ) { + throw new UserError( __( 'Sorry, you are not allowed to delete products', 'wp-graphql-woocommerce' ) ); + } + + $product_to_be_deleted = \wc_get_product( $object->get_id() ); + + if ( $force ) { + if ( $product_to_be_deleted->is_type( 'variable' ) ) { + foreach ( $product_to_be_deleted->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product_to_be_deleted->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product_to_be_deleted->delete( true ); + $result = 0 === $product_to_be_deleted->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + throw new UserError( __( 'This product does not support trashing.', 'wp-graphql-woocommerce' ) ); + } + + if ( is_callable( array( $product_to_be_deleted, 'get_status' ) ) ) { + if ( 'trash' === $product_to_be_deleted->get_status() ) { + throw new UserError( __( 'Product is already in the trash.', 'wp-graphql-woocommerce' ) ); + } + + $product_to_be_deleted->delete(); + $result = 'trash' === $product_to_be_deleted->get_status(); + } + } + + if ( ! $result ) { + throw new UserError( __( 'Failed to delete product.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 !== $product_to_be_deleted->get_parent_id() ) { + \wc_delete_product_transients( $product_to_be_deleted->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param Product $object The deleted or trashed object. + * @param array $input The mutation input. + */ + do_action( "graphql_woocommerce_delete_product_object", $object, $input ); + + return [ 'product' => $object ]; + }; + } +} diff --git a/includes/mutation/class-product-update.php b/includes/mutation/class-product-update.php new file mode 100644 index 00000000..a719b541 --- /dev/null +++ b/includes/mutation/class-product-update.php @@ -0,0 +1,71 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ Product_Create::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return array_merge( + [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + ], + Product_Create::get_input_fields() + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'product' => [ + 'type' => 'Product', + 'resolve' => static function ( $payload ) { + return new Product( $payload['id'] ); + }, + ], + ]; + } +} diff --git a/includes/mutation/class-product-variation-create.php b/includes/mutation/class-product-variation-create.php new file mode 100644 index 00000000..b070146b --- /dev/null +++ b/includes/mutation/class-product-variation-create.php @@ -0,0 +1,336 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ self::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'productId' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Description of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Unique identifier.', 'wp-graphql-woocommerce' ), + ], + 'regularPrice' => [ + 'type' => 'Float', + 'description' => __( 'Regular price of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'salePrice' => [ + 'type' => 'Float', + 'description' => __( 'Sale price of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleFrom' => [ + 'type' => 'String', + 'description' => __( 'Start date of sale price.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleTo' => [ + 'type' => 'String', + 'description' => __( 'End date of sale price.', 'wp-graphql-woocommerce' ), + ], + 'visible' => [ + 'type' => 'boolean', + 'description' => __( 'Is product variation public?', 'wp-graphql-woocommerce' ), + ], + 'virtual' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the product variation is virtual.', 'wp-graphql-woocommerce' ), + ], + 'downloadable' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the product variation is downloadable.', 'wp-graphql-woocommerce' ), + ], + 'downloads' => [ + 'type' => [ 'list_of' => 'ProductDownloadInput' ], + 'description' => __( 'Downloadable files.', 'wp-graphql-woocommerce' ), + ], + 'downloadLimit' => [ + 'type' => 'Int', + 'description' => __( 'Number of times downloadable files can be downloaded.', 'wp-graphql-woocommerce' ), + ], + 'downloadExpiry' => [ + 'type' => 'Int', + 'description' => __( 'Number of days until the download expires.', 'wp-graphql-woocommerce' ), + ], + 'taxStatus' => [ + 'type' => 'TaxStatusEnum', + 'description' => __( 'Tax status of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'taxClass' => [ + 'type' => 'String', + 'description' => __( 'Tax class of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'manageStock' => [ + 'type' => 'String', + 'description' => __( 'Whether to manage stock. Either "yes", "no", or "parent".', 'wp-graphql-woocommerce' ), + ], + 'stockQuantity' => [ + 'type' => 'Int', + 'description' => __( 'Stock quantity.', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => 'StockStatusEnum', + 'description' => __( 'Stock status of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'backorders' => [ + 'type' => 'BackordersEnum', + 'description' => __( 'Backorder status.', 'wp-graphql-woocommerce' ), + ], + 'weight' => [ + 'type' => 'String', + 'description' => __( 'Weight of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'dimensions' => [ + 'type' => 'ProductDimensionsInput', + 'description' => __( 'Dimensions of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'shippingClass' => [ + 'type' => 'String', + 'description' => __( 'Shipping class of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'image' => [ + 'type' => 'ProductImageInput', + 'description' => __( 'Image of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'attributes' => [ + 'type' => [ 'list_of' => 'ProductAttributeInput' ], + 'description' => __( 'Attributes of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'menuOrder' => [ + 'type' => 'Int', + 'description' => __( 'Menu order of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'metaData' => [ + 'type' => [ 'list_of' => 'MetaDataInput' ], + 'description' => __( 'Meta data of the product variation.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'variation' => [ + 'type' => 'ProductVariation', + 'resolve' => static function ( $payload ) { + return new Product_Variation( $payload['id'] ); + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { + if ( ! empty( $input['id'] ) ) { + $variation = \wc_get_product( $input['id'] ); + } else { + $variation = new \WC_Product_Variation(); + } + + if ( 0 === $variation->get_parent_id() ) { + $variation->set_parent_id( $input['productId'] ); + } + + if ( isset( $input['visible'] ) ) { + $variation->set_status( false === $input['visible'] ? 'private' : 'publish' ); + } + + if ( ! empty( $input['sku'] ) ) { + $variation->set_sku( wc_clean( $input['sku'] ) ); + } + + if ( ! empty( $input['image'] ) ) { + $image = $input['image']; + $image['position'] = 0; + + $variation = Product_Mutation::set_product_image( $variation, [ $image ] ); + } else { + $variation->set_image_id( '' ); + } + + if ( isset( $input['virtual'] ) ) { + $variation->set_virtual( $input['virtual'] ); + } + + if ( isset( $input['downloadable'] ) ) { + $variation->set_downloadable( $input['downloadable'] ); + } + + if ( $variation->get_downloadable() ) { + if ( ! empty( $request['downloads'] ) ) { + $variation = Product_Mutation::save_downloadable_files( $variation, $input['downloads'] ); + } + + if ( ! empty( $input['downloadLimit'] ) ) { + $variation->set_download_limit( $input['downloadLimit'] ); + } + + if ( ! empty( $input['downloadExpiry'] ) ) { + $variation->set_download_expiry( $input['downloadExpiry'] ); + } + } + + $variation = Product_Mutation::save_product_shipping_data( $variation, $input ); + + if ( isset( $input['manageStock'] ) ) { + if ( 'parent' === $input['manageStock'] ) { + $variation->set_manage_stock( false ); + } else { + $variation->set_manage_stock( wc_string_to_bool( $input['manageStock'] ) ); + } + } + + if ( isset( $input['stockStatus'] ) ) { + $variation->set_stock_status( $input['stockStatus'] ); + } + + if ( isset( $input['backorders'] ) ) { + $variation->set_backorders( $input['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $input['stockQuantity'] ) ) { + $variation->set_stock_quantity( $input['stockQuantity'] ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + if ( isset( $input['regularPrice'] ) ) { + $variation->set_regular_price( $input['regularPrice'] ); + } + + if ( isset( $input['salePrice'] ) ) { + $variation->set_sale_price( $input['salePrice'] ); + } + + if ( isset( $input['dateOnSaleFrom'] ) ) { + $variation->set_date_on_sale_from( $input['dateOnSaleFrom'] ); + } + + if ( isset( $input['dateOnSaleTo'] ) ) { + $variation->set_date_on_sale_to( $input['dateOnSaleTo'] ); + } + + if ( isset( $input['taxClass'] ) ) { + $variation->set_tax_class( $input['taxClass'] ); + } + + if ( isset( $input['description'] ) ) { + $variation->set_description( $input['description'] ); + } + + if ( ! empty( $input['attributes'] ) ) { + $attributes = []; + $parent = wc_get_product( $variation->get_parent_id() ); + $parent_attributes = $parent->get_attributes(); + + foreach ( $input['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['attributeName'] ) ) { + $raw_attribute_name = sanitize_title( $attribute['attributeName'] ); + } + + if ( ! $attribute_id && ! $raw_attribute_name ) { + continue; + } + + $attribute_name = sanitize_title( $raw_attribute_name ); + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + if ( ! empty( $input['menuOrder'] ) ) { + $variation->set_menu_order( $input['menuOrder'] ); + } + + if ( ! empty( $input['metaData'] ) ) { + foreach ( $input['metaData'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + $variation_id = $variation->save(); + + return [ 'id' => $variation_id ]; + } +} diff --git a/includes/mutation/class-product-variation-delete.php b/includes/mutation/class-product-variation-delete.php new file mode 100644 index 00000000..75a6980b --- /dev/null +++ b/includes/mutation/class-product-variation-delete.php @@ -0,0 +1,145 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => self::mutate_and_get_payload(), + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + 'force' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'variation' => [ + 'type' => 'ProductVariation', + 'resolve' => static function ( $payload ) { + return $payload['variation']; + }, + ], + ]; + } + + /** + * Defines the mutation data modification closure. + * + * @return callable + */ + public static function mutate_and_get_payload() { + return static function ( $input, AppContext $context, ResolveInfo $info ) { + $variation_id = $input['id']; + $force = isset( $input['force'] ) ? $input['force'] : false; + $object = new Product_Variation( $variation_id ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + throw new UserError( __( 'Invalid product variation ID.', 'wp-graphql-woocommerce' ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( [ $object, 'get_status' ] ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param \WC_Product $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "graphql_woocommerce_product_variation_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( 'product_variation', 'delete', $object->get_id() ) ) { + throw new UserError( __( 'Sorry, you are not allowed to delete product variations', 'wp-graphql-woocommerce' ) ); + } + + $variation_to_be_deleted = \wc_get_product( $object->get_id() ); + + if ( $force ) { + $variation_to_be_deleted->delete( true ); + $result = 0 === $variation_to_be_deleted->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + throw new UserError( __( 'This product variation does not support trashing.', 'wp-graphql-woocommerce' ) ); + } + + if ( is_callable( array( $variation_to_be_deleted, 'get_status' ) ) ) { + if ( 'trash' === $variation_to_be_deleted->get_status() ) { + throw new UserError( __( 'Product variation is already in the trash.', 'wp-graphql-woocommerce' ) ); + } + + $variation_to_be_deleted->delete(); + $result = 'trash' === $variation_to_be_deleted->get_status(); + } + } + + if ( ! $result ) { + throw new UserError( __( 'Failed to delete product variation.', 'wp-graphql-woocommerce' ) ); + } + + if ( 0 !== $variation_to_be_deleted->get_parent_id() ) { + \wc_delete_product_transients( $variation_to_be_deleted->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param Product_Variation $object The deleted or trashed object. + * @param array $input The mutation input. + */ + do_action( "graphql_woocommerce_delete_product_variation_object", $object, $input ); + + return [ 'variation' => $object ]; + }; + } +} diff --git a/includes/mutation/class-product-variation-update.php b/includes/mutation/class-product-variation-update.php new file mode 100644 index 00000000..686c8bee --- /dev/null +++ b/includes/mutation/class-product-variation-update.php @@ -0,0 +1,71 @@ + self::get_input_fields(), + 'outputFields' => self::get_output_fields(), + 'mutateAndGetPayload' => [ Product_Variation_Create::class, 'mutate_and_get_payload' ], + ] + ); + } + + /** + * Defines the mutation input field configuration + * + * @return array + */ + public static function get_input_fields() { + return array_merge( + [ + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + ], + Product_Variation_Create::get_input_fields() + ); + } + + /** + * Defines the mutation output field configuration + * + * @return array + */ + public static function get_output_fields() { + return [ + 'variation' => [ + 'type' => 'ProductVariation', + 'resolve' => static function ( $payload ) { + return new Product_Variation( $payload['id'] ); + }, + ], + ]; + } +} diff --git a/includes/type/input/class-product-attribute-input.php b/includes/type/input/class-product-attribute-input.php index 2ef5e1a4..d865a805 100644 --- a/includes/type/input/class-product-attribute-input.php +++ b/includes/type/input/class-product-attribute-input.php @@ -29,6 +29,9 @@ public static function register() { 'attributeValue' => [ 'type' => 'String', ], + 'id' => [ + 'type' => 'Int', + ], ], ] ); diff --git a/includes/type/input/class-product-attributes-input.php b/includes/type/input/class-product-attributes-input.php new file mode 100644 index 00000000..3be70ac3 --- /dev/null +++ b/includes/type/input/class-product-attributes-input.php @@ -0,0 +1,54 @@ + __( 'Product attribute properties', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'id' => [ + 'type' => 'Int', + 'description' => __( 'Attribute ID', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Attribute name', 'wp-graphql-woocommerce' ), + ], + 'position' => [ + 'type' => 'Int', + 'description' => __( 'Attribute position', 'wp-graphql-woocommerce' ), + ], + 'visible' => [ + 'type' => 'Boolean', + 'description' => __( 'Define if the attribute is visible on the "Additional information" tab in the product\'s page. Default is false.', 'wp-graphql-woocommerce' ), + ], + 'variation' => [ + 'type' => 'Boolean', + 'description' => __( 'Define if the attribute can be used as variation. Default is false.', 'wp-graphql-woocommerce' ), + ], + 'options' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'List of available term names for the attribute', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/input/class-product-dimensions-input.php b/includes/type/input/class-product-dimensions-input.php new file mode 100644 index 00000000..7ee40795 --- /dev/null +++ b/includes/type/input/class-product-dimensions-input.php @@ -0,0 +1,42 @@ + __( 'Product dimensions', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'length' => [ + 'type' => 'String', + 'description' => __( 'Length of the product', 'wp-graphql-woocommerce' ), + ], + 'width' => [ + 'type' => 'String', + 'description' => __( 'Width of the product', 'wp-graphql-woocommerce' ), + ], + 'height' => [ + 'type' => 'String', + 'description' => __( 'Height of the product', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/input/class-product-download-input.php b/includes/type/input/class-product-download-input.php new file mode 100644 index 00000000..38d5a3c7 --- /dev/null +++ b/includes/type/input/class-product-download-input.php @@ -0,0 +1,42 @@ + __( 'Product download', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'id' => [ + 'type' => 'Int', + 'description' => __( 'File ID', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'File name', 'wp-graphql-woocommerce' ), + ], + 'file' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'File URL', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/input/class-product-image-input.php b/includes/type/input/class-product-image-input.php new file mode 100644 index 00000000..27490291 --- /dev/null +++ b/includes/type/input/class-product-image-input.php @@ -0,0 +1,46 @@ + __( 'Product image', 'wp-graphql-woocommerce' ), + 'fields' => [ + 'id' => [ + 'type' => 'Int', + 'description' => __( 'Image ID', 'wp-graphql-woocommerce' ), + ], + 'src' => [ + 'type' => 'String', + 'description' => __( 'Image URL', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Image name', 'wp-graphql-woocommerce' ), + ], + 'altText' => [ + 'type' => 'String', + 'description' => __( 'Image alternative text', 'wp-graphql-woocommerce' ), + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-product-attribute-object-type.php b/includes/type/object/class-product-attribute-object-type.php new file mode 100644 index 00000000..51029aca --- /dev/null +++ b/includes/type/object/class-product-attribute-object-type.php @@ -0,0 +1,75 @@ + __( 'Product attribute object.', 'wp-graphql-woocommerce' ), + 'eagerlyLoadType' => true, + 'fields' => [ + 'id' => [ + 'type' => 'ID', + 'description' => __( 'Unique identifier for the product attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->attribute_id ) ? $source->attribute_id : null; + }, + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->attribute_name ) ? (string) $source->attribute_name : null; + }, + ], + 'label' => [ + 'type' => 'String', + 'description' => __( 'Label of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->attribute_label ) ? (string) $source->attribute_label : null; + }, + ], + 'type' => [ + 'type' => 'String', + 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->attribute_type ) ? (string) $source->attribute_type : null; + }, + ], + 'orderBy' => [ + 'type' => 'String', + 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->attribute_orderby ) ? (string) $source->attribute_orderby : null; + }, + ], + 'hasArchives' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether or not the attribute has archives.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return isset( $source->attribute_public ) ? $source->attribute_public : false; + }, + ], + ], + ] + ); + } +} \ No newline at end of file diff --git a/includes/type/object/class-product-attribute-term-object-type.php b/includes/type/object/class-product-attribute-term-object-type.php new file mode 100644 index 00000000..6ab2f0d1 --- /dev/null +++ b/includes/type/object/class-product-attribute-term-object-type.php @@ -0,0 +1,75 @@ + __( 'Product attribute object.', 'wp-graphql-woocommerce' ), + 'eagerlyLoadType' => true, + 'fields' => [ + 'id' => [ + 'type' => 'Integer', + 'description' => __( 'Unique identifier for the product attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->id ) ? $source->id : null; + }, + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->name ) ? $source->name : null; + }, + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Label of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->slug ) ? $source->slug : null; + }, + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return ! empty( $source->description ) ? $source->description : null; + }, + ], + 'menuOrder' => [ + 'type' => 'Integer', + 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return isset( $source->menu_order ) ? $source->menu_order : 0; + }, + ], + 'count' => [ + 'type' => 'Integer', + 'description' => __( 'Whether or not the attribute has archives.', 'wp-graphql-woocommerce' ), + 'resolve' => static function( $source ) { + return isset( $source->count ) ? $source->count : 0; + }, + ], + ], + ] + ); + } +} \ No newline at end of file diff --git a/tests/_support/Factory/ProductFactory.php b/tests/_support/Factory/ProductFactory.php index dc7d816c..e5cd84a1 100644 --- a/tests/_support/Factory/ProductFactory.php +++ b/tests/_support/Factory/ProductFactory.php @@ -174,6 +174,8 @@ 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, @@ -184,6 +186,11 @@ public function createAttribute( $raw_name = 'size', $terms = [ 'small' ] ) { ] ); + 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, @@ -232,6 +239,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 +394,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/TestCase/WooGraphQLTestCase.php b/tests/_support/TestCase/WooGraphQLTestCase.php index 0019ac79..541dcf0c 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/ProductAttributeMutationsTest.php b/tests/wpunit/ProductAttributeMutationsTest.php index 71877f55..8ccb29b0 100644 --- a/tests/wpunit/ProductAttributeMutationsTest.php +++ b/tests/wpunit/ProductAttributeMutationsTest.php @@ -2,14 +2,177 @@ class ProductAttributeMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { public function testCreateProductAttribute() { - $this->markTestIncomplete(); + $query = ' + mutation ($input: CreateProductAttributeInput!) { + createProductAttribute(input: $input) { + attribute { + id + name + label + type + orderBy + hasArchives + } + } + } + '; + + $variables = [ + 'input' => [ + 'name' => 'Pattern', + 'slug' => 'pattern', + 'orderBy' => 'menu_order', + 'hasArchives' => false, + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProductAttribute.attribute', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'pattern' ), + $this->expectedField( 'label', 'Pattern' ), + $this->expectedField( 'type', 'select' ), + $this->expectedField( 'orderBy', 'menu_order' ), + $this->expectedField( 'hasArchives', false ), + ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); } public function testUpdateProductAttribute() { - $this->markTestIncomplete(); + $query = ' + mutation ($input: CreateProductAttributeInput!) { + createProductAttribute(input: $input) { + attribute { + id + name + label + type + orderBy + hasArchives + } + } + } + '; + + $variables = [ + 'input' => [ + 'name' => 'Pattern', + 'slug' => 'pattern', + 'orderBy' => 'menu_order', + 'hasArchives' => false, + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProductAttribute.attribute', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'hasArchives', false ), + ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + $attribute_id = $this->lodashGet( $response, 'data.createProductAttribute.attribute.id' ); + $this->assertNotEmpty( $attribute_id ); + + $query = ' + mutation ($input: UpdateProductAttributeInput!) { + updateProductAttribute(input: $input) { + attribute { + id + name + label + type + orderBy + hasArchives + } + } + } + '; + + $variables = [ + 'input' => [ + 'id' => $attribute_id, + 'name' => 'Pattern', + 'slug' => 'pattern', + 'orderBy' => 'menu_order', + 'hasArchives' => true, + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateProductAttribute.attribute', + [ + $this->expectedField( 'id', $attribute_id ), + $this->expectedField( 'name', 'pattern' ), + $this->expectedField( 'label', 'Pattern' ), + $this->expectedField( 'type', 'select' ), + $this->expectedField( 'orderBy', 'menu_order' ), + $this->expectedField( 'hasArchives', true ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); } public function testDeleteProductAttribute() { - $this->markTestIncomplete(); + $query = ' + mutation ($input: CreateProductAttributeInput!) { + createProductAttribute(input: $input) { + attribute { + id + } + } + } + '; + + $variables = [ + 'input' => [ + 'name' => 'Pattern', + 'slug' => 'pattern', + 'orderBy' => 'menu_order', + 'hasArchives' => false, + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQuerySuccessful( $response ); + + $attribute_id = $this->lodashGet( $response, 'data.createProductAttribute.attribute.id' ); + $this->assertNotEmpty( $attribute_id ); + + $query = ' + mutation ($input: DeleteProductAttributeInput!) { + deleteProductAttribute(input: $input) { + attribute { + id + } + } + } + '; + + $variables = [ + 'input' => [ + 'id' => $attribute_id, + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteProductAttribute.attribute', + [ + $this->expectedField( 'id', $attribute_id ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); } -} \ No newline at end of file +} diff --git a/tests/wpunit/ProductAttributeTermMutationsTest.php b/tests/wpunit/ProductAttributeTermMutationsTest.php new file mode 100644 index 00000000..2f70a8f3 --- /dev/null +++ b/tests/wpunit/ProductAttributeTermMutationsTest.php @@ -0,0 +1,133 @@ +factory()->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + + $query = ' + mutation ($input: CreateProductAttributeTermInput!) { + createProductAttributeTerm(input: $input) { + term { + id + name + slug + description + menuOrder + count + } + } + } + '; + + $variables = [ + 'input' => [ + 'attributeId' => $kind_attribute['attribute_id'], + 'name' => 'Hated', + 'slug' => 'hated', + 'description' => 'Hated by all', + 'menuOrder' => 2, + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProductAttributeTerm.term', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Hated' ), + $this->expectedField( 'slug', 'hated' ), + $this->expectedField( 'description', 'Hated by all' ), + $this->expectedField( 'menuOrder', 2 ), + $this->expectedField( 'count', 0 ), + ] + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testUpdateProductAttributeTerm() { + $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special', 'hated' ] ); + $hated_term_id = get_term_by( 'slug', 'hated', 'pa_kind' )->term_id; + + $query = ' + mutation ($input: UpdateProductAttributeTermInput!) { + updateProductAttributeTerm(input: $input) { + term { + id + name + slug + description + menuOrder + count + } + } + } + '; + + $variables = [ + 'input' => [ + 'attributeId' => $kind_attribute['attribute_id'], + 'id' => $hated_term_id, + 'name' => 'Loved', + 'slug' => 'loved', + 'description' => 'Loved by all', + 'menuOrder' => 0, + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateProductAttributeTerm.term', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Loved' ), + $this->expectedField( 'slug', 'loved' ), + $this->expectedField( 'description', 'Loved by all' ), + $this->expectedField( 'menuOrder', 0 ), + $this->expectedField( 'count', 0 ), + ] + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testDeleteProductAttributeTerm() { + $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special', 'hated' ] ); + $hated_term_id = get_term_by( 'slug', 'hated', 'pa_kind' )->term_id; + + $query = ' + mutation ($input: DeleteProductAttributeTermInput!) { + deleteProductAttributeTerm(input: $input) { + term { + id + slug + } + } + } + '; + + $variables = [ + 'input' => [ + 'attributeId' => $kind_attribute['attribute_id'], + 'id' => $hated_term_id, + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteProductAttributeTerm.term', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'slug', 'hated' ), + ] + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } +} diff --git a/tests/wpunit/ProductMutationsTest.php b/tests/wpunit/ProductMutationsTest.php index d5be93aa..f64895de 100644 --- a/tests/wpunit/ProductMutationsTest.php +++ b/tests/wpunit/ProductMutationsTest.php @@ -2,14 +2,531 @@ class ProductMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { public function testCreateProduct() { - $this->markTestIncomplete(); + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + dateOnSaleFrom + dateOnSaleTo + ... on ProductWithPricing { + price + regularPrice + salePrice + } + ... on InventoriedProduct { + stockQuantity + } + attributes { + nodes { + id + name + label + options + } + } + } + } + } + '; + + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + 'salePrice' => 7, + 'dateOnSaleFrom' => date( 'Y-m-d H:i:s', strtotime( '-1 day' ) ), + 'dateOnSaleTo' => date( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'manageStock' => true, + 'stockStatus' => 'IN_STOCK', + 'stockQuantity' => 3, + ] + ]; + + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedField( 'price', "$7.00" ), + $this->expectedField( 'regularPrice', "$10.00" ), + $this->expectedField( 'salePrice', "$7.00" ), + $this->expectedField( 'stockQuantity', 3 ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreateProductWithAttributes() { + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + attributes { + nodes { + id + name + label + options + } + } + } + } + } + '; + + $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + 'attributes' => [ + [ + 'id' => $kind_attribute['attribute_id'], + 'name' => $kind_attribute['attribute_name'], + 'options' => [ 'special' ], + ], + ], + ] + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedObject( + 'attributes', + [ + $this->expectedNode( + 'nodes', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'pa_kind' ), + $this->expectedField( 'label', 'Kind' ), + $this->expectedField( 'options', [ 'special' ] ), + ], + 0 + ), + ] + ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreatingVariableProductWithCreateProduct() { + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + ... on VariableProduct { + attributes { + nodes { + id + name + label + options + variation + scope + } + } + defaultAttributes { + nodes { + id + name + label + value + } + } + } + } + } + } + '; + + $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'VARIABLE', + 'attributes' => [ + [ + 'id' => $kind_attribute['attribute_id'], + 'name' => $kind_attribute['attribute_name'], + 'options' => [ 'normal', 'special', ], + 'variation' => true, + ], + [ + 'name' => 'logo', + 'options' => [ 'yes', 'no' ], + 'variation' => true, + ], + ], + 'defaultAttributes' => [ + [ + 'id' => $kind_attribute['attribute_id'], + 'attributeName' => $kind_attribute['attribute_name'], + 'attributeValue' => 'special', + ], + [ + 'attributeName' => 'logo', + 'attributeValue' => 'yes', + ] + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'VARIABLE' ), + $this->expectedObject( + 'attributes', + [ + $this->expectedNode( + 'nodes', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'pa_kind' ), + $this->expectedField( 'label', 'Kind' ), + $this->expectedField( 'options', [ 'normal', 'special' ] ), + $this->expectedField( 'variation', true ), + $this->expectedField( 'scope', 'GLOBAL' ), + ], + ), + $this->expectedNode( + 'nodes', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'logo' ), + $this->expectedField( 'label', 'Logo' ), + $this->expectedField( 'options', [ 'yes', 'no' ] ), + $this->expectedField( 'variation', true ), + $this->expectedField( 'scope', 'LOCAL' ), + ], + ), + ] + ), + $this->expectedObject( + 'defaultAttributes', + [ + $this->expectedNode( + 'nodes', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'pa_kind' ), + $this->expectedField( 'label', 'Kind' ), + $this->expectedField( 'value', 'special' ), + ], + ), + $this->expectedNode( + 'nodes', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'logo' ), + $this->expectedField( 'label', 'Logo' ), + $this->expectedField( 'value', 'yes' ), + ], + ), + ] + ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreatingExternalProductWithCreateProduct() { + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + ... on ExternalProduct { + externalUrl + buttonText + } + } + } + } + '; + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'EXTERNAL', + 'externalUrl' => 'https://example.com', + 'buttonText' => 'Buy Now', + ] + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'EXTERNAL' ), + $this->expectedField( 'externalUrl', 'https://example.com' ), + $this->expectedField( 'buttonText', 'Buy Now' ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreatingGroupProductWithCreateProduct() { + $product_ids = [ + $this->factory()->product->createSimple( [ 'name' => 'Product 1' ] ), + $this->factory()->product->createSimple( [ 'name' => 'Product 2' ] ), + ]; + + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + ... on GroupProduct { + products { + nodes { + id + name + } + } + } + } + } + } + '; + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'GROUPED', + 'groupedProducts' => $product_ids, + ] + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'GROUPED' ), + $this->expectedField( 'products.nodes.#.name', 'Product 1' ), + $this->expectedField( 'products.nodes.#.name', 'Product 2' ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); } public function testUpdateProduct() { - $this->markTestIncomplete(); + $product_id = $this->factory()->product->createSimple( + [ + 'name' => 'Test Product', + 'regular_price' => 10, + 'sale_price' => 7, + 'manage_stock' => true, + 'stock_status' => 'instock', + 'stock_quantity' => 3, + ] + ); + + $query = ' + mutation ( $input: UpdateProductInput! ) { + updateProduct(input: $input) { + product { + id + name + type + ... on ProductWithPricing { + price + regularPrice + salePrice + } + ... on InventoriedProduct { + stockQuantity + } + attributes { + nodes { + id + name + label + options + } + } + } + } + } + '; + + $variables = [ + 'input' => [ + 'id' => $product_id, + 'name' => 'Updated Product', + 'type' => 'SIMPLE', + 'regularPrice' => 19.99, + 'salePrice' => 14.99, + 'stockQuantity' => 5, + ] + ]; + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Updated Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedField( 'price', "$14.99" ), + $this->expectedField( 'regularPrice', "$19.99" ), + $this->expectedField( 'salePrice', "$14.99" ), + $this->expectedField( 'stockQuantity', 5 ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); } public function testDeleteProduct() { - $this->markTestIncomplete(); + $product_id = $this->factory()->product->createSimple([ + 'name' => 'Test Product', + 'regular_price' => 10, + 'sale_price' => 7, + 'manage_stock' => true, + 'stock_status' => 'instock', + 'stock_quantity' => 3, + ]); + + $query = ' + mutation ( $input: DeleteProductInput! ) { + deleteProduct(input: $input) { + product { + id + name + type + ... on ProductWithPricing { + price + regularPrice + salePrice + } + ... on InventoriedProduct { + stockQuantity + } + attributes { + nodes { + id + name + label + options + } + } + } + } + } + '; + + $variables = [ + 'input' => [ + 'id' => $product_id, + 'force' => true, + ], + ]; + + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedField( 'price', "$7.00" ), + $this->expectedField( 'regularPrice', "$10.00" ), + $this->expectedField( 'salePrice', "$7.00" ), + $this->expectedField( 'stockQuantity', 3 ), + ] + ), + ]; + $this->assertQuerySuccessful( $response, $expected ); + + // Assert product is deleted. + $product = wc_get_product( $product_id ); + $this->assertFalse( $product ); } } \ No newline at end of file diff --git a/tests/wpunit/ProductVariationMutationsTest.php b/tests/wpunit/ProductVariationMutationsTest.php new file mode 100644 index 00000000..e343446f --- /dev/null +++ b/tests/wpunit/ProductVariationMutationsTest.php @@ -0,0 +1,175 @@ +factory->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + $product_id = $this->factory->product->createVariable( + [ + 'attributes' => [ + $this->factory->product->createAttributeObject( + $kind_attribute['attribute_id'], + $kind_attribute['attribute_taxonomy'], + $kind_attribute['term_ids'], + 0, + true, + true, + ), + ], + 'attribute_data' => [], + ] + ); + + $query = ' + mutation ($input: CreateProductVariationInput!) { + createProductVariation(input: $input) { + variation { + id + parent { node { id } } + price + regularPrice + salePrice + attributes { + nodes { + id + name + value + } + } + } + } + } + '; + + $variables = [ + 'input' => [ + 'productId' => $product_id, + 'regularPrice' => 14.99, + 'salePrice' => 9.99, + 'attributes' => [ + [ + 'id' => $kind_attribute['attribute_id'], + 'attributeName' => $kind_attribute['attribute_name'], + 'attributeValue' => 'special', + ], + ], + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProductVariation.variation', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'parent.node.id', $this->toRelayId( 'product', $product_id ) ), + $this->expectedField( 'price', "$9.99" ), + $this->expectedField( 'regularPrice', "$14.99" ), + $this->expectedField( 'salePrice', "$9.99" ), + $this->expectedNode( + 'attributes.nodes', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', $kind_attribute['attribute_taxonomy'] ), + $this->expectedField( 'value', 'special' ), + ] + ), + ] + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testUpdateProductVariation() { + $ids = $this->factory->product_variation->createSome( + $this->factory->product->createVariable() + ); + + $query = ' + mutation ($input: UpdateProductVariationInput!) { + updateProductVariation(input: $input) { + variation { + id + parent { node { id } } + price + regularPrice + salePrice + } + } + } + '; + + $variables = [ + 'input' => [ + 'productId' => $ids['product'], + 'id' => $ids['variations'][0], + 'regularPrice' => 19.99, + 'salePrice' => 14.99, + ], + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'updateProductVariation.variation', + [ + $this->expectedField( 'id', $this->toRelayId( 'product_variation', $ids['variations'][0] ) ), + $this->expectedField( 'parent.node.id', $this->toRelayId( 'product', $ids['product'] ) ), + $this->expectedField( 'price', "$14.99" ), + $this->expectedField( 'regularPrice', "$19.99" ), + $this->expectedField( 'salePrice', "$14.99" ), + ] + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testDeleteProductVariation() { + $ids = $this->factory->product_variation->createSome( + $this->factory->product->createVariable() + ); + + $query = ' + mutation ($input: DeleteProductVariationInput!) { + deleteProductVariation(input: $input) { + variation { + id + parent { node { id } } + price + } + } + } + '; + + $variables = [ + 'input' => [ + 'id' => $ids['variations'][0], + 'force' => true, + ], + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'deleteProductVariation.variation', + [ + $this->expectedField( 'id', $this->toRelayId( 'product_variation', $ids['variations'][0] ) ), + $this->expectedField( 'parent.node.id', $this->toRelayId( 'product', $ids['product'] ) ), + $this->expectedField( 'price', self::NOT_NULL ), + ] + ) + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + // $variation = wc_get_product( $ids['variations'][0] ); + // $this->assertFalse( $variation ); + } +} From 1b0db1f0c78eb559d4e1933dfb1288bd689fd53c Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 9 May 2024 01:03:21 -0400 Subject: [PATCH 3/3] chore: Linter and PHPStan compliance met --- .../data/mutation/class-product-mutation.php | 558 ++++++----- includes/model/class-product.php | 1 + .../class-product-attribute-create.php | 43 +- .../class-product-attribute-delete.php | 38 +- .../class-product-attribute-term-create.php | 90 +- .../class-product-attribute-term-delete.php | 52 +- .../class-product-attribute-term-update.php | 17 +- .../class-product-attribute-update.php | 36 +- includes/mutation/class-product-create.php | 896 +++++++++--------- includes/mutation/class-product-delete.php | 57 +- includes/mutation/class-product-update.php | 12 +- .../class-product-variation-create.php | 561 +++++------ .../class-product-variation-delete.php | 59 +- .../class-product-variation-update.php | 12 +- .../input/class-product-attributes-input.php | 48 +- .../input/class-product-dimensions-input.php | 22 +- .../input/class-product-download-input.php | 24 +- .../type/input/class-product-image-input.php | 32 +- .../class-product-attribute-object-type.php | 104 +- ...ass-product-attribute-term-object-type.php | 104 +- phpstan/constants.php | 1 + tests/_data/test-product.jpg | Bin 0 -> 23041 bytes tests/_support/Factory/ProductFactory.php | 9 + .../wpunit/ProductAttributeMutationsTest.php | 45 + .../ProductAttributeTermMutationsTest.php | 42 +- tests/wpunit/ProductMutationsTest.php | 324 ++++++- 26 files changed, 1871 insertions(+), 1316 deletions(-) create mode 100644 tests/_data/test-product.jpg diff --git a/includes/data/mutation/class-product-mutation.php b/includes/data/mutation/class-product-mutation.php index 2dc1c0a7..c68b1dcd 100644 --- a/includes/data/mutation/class-product-mutation.php +++ b/includes/data/mutation/class-product-mutation.php @@ -14,258 +14,307 @@ * Class - Product_Mutation */ class Product_Mutation { - /** - * Save product shipping data - * - * @param \WC_Product $product Product object. - * @param array $input Mutation input. - * - * @return \WC_Product - */ - public static function save_product_shipping_data( $product, $input ) { - // Virtual - if ( isset( $input['virtual'] ) && true === $input['virtual'] ) { - $product->set_weight( '' ); + /** + * Save product shipping data + * + * @param \WC_Product $product Product object. + * @param array $input Mutation input. + * + * @return \WC_Product + */ + public static function save_product_shipping_data( $product, $input ) { + // Virtual. + if ( isset( $input['virtual'] ) && true === $input['virtual'] ) { + $product->set_weight( '' ); $product->set_height( '' ); $product->set_length( '' ); $product->set_width( '' ); - } else { - if ( isset( $input['weight'] ) ) { - $product->set_weight( $input['weight'] ); - } - - if ( isset( $input['dimensions']['height'] ) ) { - $product->set_height( $input['dimensions']['height'] ); - } - - if ( isset( $input['dimensions']['width'] ) ) { - $product->set_width( $input['dimensions']['width'] ); - } - - if ( isset( $input['dimensions']['length'] ) ) { - $product->set_length( $input['dimensions']['length'] ); - } - } - - if ( isset( $input['shippingClass'] ) ) { - $data_store = $product->get_data_store(); - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $input['shippingClass'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - - return $product; - } - - /** - * Prepare product attribute - * - * @param array $attribute Product attribute data. - * - * @return \WC_Product_Attribute|null - */ - public static function prepare_attribute( $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = wc_clean( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - return null; - } - - if ( $attribute_id ) { - if ( isset( $attribute['options'] ) ) { - $options = $attribute['options']; - - if ( ! is_array( $options ) ) { - $options = explode( WC_DELIMITER, $options ); - } - - $values = array_map( 'wc_sanitize_term_text_based', $options ); - $values = array_filter( $values, 'strlen' ); - } else { - $values = []; - } - - if ( ! empty( $values ) ) { - $attribute_object = new \WC_Product_Attribute(); - $attribute_object->set_id( $attribute_id ); - $attribute_object->set_name( $attribute_name ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - - return $attribute_object; - } - } elseif ( isset( $attribute['options'] ) ) { - if ( is_array( $attribute['options'] ) ) { - $values = $attribute['options']; - } else { - $values = explode( WC_DELIMITER, $attribute['options'] ); - } - - $attribute_object = new \WC_Product_Attribute(); - $attribute_object->set_name( $attribute_name ); - $attribute_object->set_options( $values ); - $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); - $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); - $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); - - return $attribute_object; - } - - return null; - } - - /** - * Save product attributes - * - * @param WC_Data $product Product instance. - * @param array $terms Terms data. - * @param string $taxonomy Taxonomy name. - * - * @return \WC_Data - */ - public static function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { - $term_ids = wp_list_pluck( $terms, 'id' ); + } else { + if ( isset( $input['weight'] ) ) { + $product->set_weight( $input['weight'] ); + } + + if ( isset( $input['dimensions']['height'] ) ) { + $product->set_height( $input['dimensions']['height'] ); + } + + if ( isset( $input['dimensions']['width'] ) ) { + $product->set_width( $input['dimensions']['width'] ); + } + + if ( isset( $input['dimensions']['length'] ) ) { + $product->set_length( $input['dimensions']['length'] ); + } + } + + if ( isset( $input['shippingClass'] ) ) { + /** + * Shipping class. + * + * @var string $shippingClass + */ + $shippingClass = wc_clean( $input['shippingClass'] ); + /** + * WC product data store instance. + * + * @var \WC_Product_Data_Store_Interface $data_store + */ + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( $shippingClass ); + if ( $shipping_class_id ) { + $product->set_shipping_class_id( $shipping_class_id ); + } else { + graphql_debug( + /* translators: %s: Shipping class */ + sprintf( __( 'Invalid shipping class: %s', 'wp-graphql-woocommerce' ), $shippingClass ) + ); + } + } + + return $product; + } + + /** + * Prepare product attribute + * + * @param array $attribute Product attribute data. + * + * @return \WC_Product_Attribute|null + */ + public static function prepare_attribute( $attribute ) { + /** + * Attribute ID. + * + * @var int $attribute_id + */ + $attribute_id = 0; + /** + * Attribute name. + * + * @var string $attribute_name + */ + $attribute_name = ''; + + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + /** + * @var string $attribute_name + */ + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + return null; + } + + if ( $attribute_id ) { + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $options ) ) { + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = []; + } + + if ( ! empty( $values ) ) { + $attribute_object = new \WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( isset( $attribute['visible'] ) ? $attribute['visible'] : false ); + $attribute_object->set_variation( isset( $attribute['variation'] ) ? $attribute['variation'] : false ); + + return $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new \WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( isset( $attribute['visible'] ) ? $attribute['visible'] : false ); + $attribute_object->set_variation( isset( $attribute['variation'] ) ? $attribute['variation'] : false ); + + return $attribute_object; + } + + return null; + } + + /** + * Save product attributes + * + * @param \WC_Product $product Product instance. + * @param array $term_ids Terms IDs. + * @param string $taxonomy Taxonomy name. + * + * @return \WC_Product + */ + public static function save_taxonomy_terms( $product, $term_ids, $taxonomy = 'cat' ) { if ( 'cat' === $taxonomy ) { $product->set_category_ids( $term_ids ); } elseif ( 'tag' === $taxonomy ) { $product->set_tag_ids( $term_ids ); } - } - - /** - * Save product downloadable files - * - * @param WC_Data $product Product instance. - * @param array $downloads Downloads data. - * - * @return \WC_Data - */ - public static function save_downloadable_files( $product, $downloads ) { - $files = array(); + + return $product; + } + + /** + * Save product downloadable files + * + * @param \WC_Product|\WC_Product_Variation $product Product instance. + * @param array $downloads Downloads data. + * + * @return \WC_Product|\WC_Product_Variation + */ + public static function save_downloadable_files( $product, $downloads ) { + $files = []; foreach ( $downloads as $key => $file ) { if ( empty( $file['file'] ) ) { continue; } - $download = new WC_Product_Download(); + $download = new \WC_Product_Download(); $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $files[] = $download; } + $product->set_downloads( $files ); - return $product; - } - - /** - * Save variable product default attributes - * - * @param \WC_Data $product Product object. - * @param array $input Mutation input. - * - * @return \WC_Data - */ - public static function save_default_attributes( $product, $input ) { - if ( ! empty( $input['defaultAttributes'] ) ) { - - $attributes = $product->get_attributes(); - $default_attributes = []; - - foreach ( $input['defaultAttributes'] as $attribute ) { - $attribue_id = 0; - $attribute_name = ''; - - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['attributeName'] ) ) { - $attribute_name = sanitize_title( $attribute['attributeName'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( isset( $attributes[ $attribute_name ] ) ) { - $_attribute = $attributes[ $attribute_name ]; - - if ( $_attribute['is_variation'] ) { - $value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : ''; - - if ( ! empty( $_attribute['is_taxonomy'] ) ) { - $term = get_term_by( 'name', $value, $attribute_name ); - - if ( $term && ! is_wp_error( $term ) ) { - $value = $term->slug; - } else { - $value = $sanitize_title( $value ); - } - } - - if ( $value ) { - $default_attributes[ $attribute_name ] = $value; - } - } - } - } - - $product->set_default_attributes( $default_attributes ); - } - - return $product; - } - - /** - * Set product images - * - * @param WC_Product $product Product instance. - * @param array $images Images data. - * - * @return \WC_Product - */ - public static function set_product_images( $product, $images ) { - $images = is_array( $images ) ? array_filter( $images ) : []; - - if ( ! empty( $images ) ) { - $gallery_positions = []; - - foreach ( $images as $index => $image ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = \wc_rest_upload_image_from_url( $image['src'] ); - - if ( is_wp_error( $upload ) ) { - if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { - throw new UserError( $upload->get_error_message() ); - } else { - continue; - } - } - - $attachment_id = \wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); - } - - if ( ! wp_attachment_is_image( $attachment_id ) ) { - throw new UserError( - sprintf( __( '#%s is an invalid image ID.', 'wp-graphql-woocommerce' ), $attachment_id ) - ); - } - - $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); - - // Set the image alt if present. + return $product; + } + + /** + * Save variable product default attributes + * + * @param \WC_Product $product Product object. + * @param array $input Mutation input. + * + * @return \WC_Product + */ + public static function save_default_attributes( $product, $input ) { + if ( ! empty( $input['defaultAttributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = []; + + foreach ( $input['defaultAttributes'] as $attribute ) { + /** + * Attribute ID. + * + * @var int $attribute_id + */ + $attribute_id = 0; + /** + * Attribute name. + * + * @var string $attribute_name + */ + $attribute_name = ''; + + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['attributeName'] ) ) { + $attribute_name = sanitize_title( $attribute['attributeName'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + /** + * Attribute value. + * + * @var string $value + */ + $value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Set product images + * + * @param \WC_Product $product Product instance. + * @param array $images Images data. + * + * @throws \GraphQL\Error\UserError If image upload fails | Invalid image ID. + * + * @return \WC_Product + */ + public static function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : []; + + if ( ! empty( $images ) ) { + $gallery_positions = []; + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = \wc_rest_upload_image_from_url( $image['src'] ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + throw new UserError( $upload->get_error_message() ); + } else { + continue; + } + } + + $attachment_id = \wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new UserError( + /* translators: %s: Attachment ID */ + sprintf( __( '#%s is an invalid image ID.', 'wp-graphql-woocommerce' ), $attachment_id ) + ); + } + + $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); + + // Set the image alt if present. if ( ! empty( $image['altText'] ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); } @@ -276,7 +325,7 @@ public static function set_product_images( $product, $images ) { [ 'ID' => $attachment_id, 'post_title' => $image['name'], - ] + ] ); } @@ -284,32 +333,45 @@ public static function set_product_images( $product, $images ) { if ( ! empty( $image['src'] ) ) { update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); } - } + } - // Sort images and get IDs in correct order. + // Sort images and get IDs in correct order. asort( $gallery_positions ); // Get gallery in correct order. $gallery = array_keys( $gallery_positions ); - // Featured image is in position 0. + /** + * Featured image ID in position 0. + * + * @var int $image_id + */ $image_id = array_shift( $gallery ); // Set images. $product->set_image_id( $image_id ); $product->set_gallery_image_ids( $gallery ); - } else { - $product->set_image_id( '' ); + } else { + $product->set_image_id( '' ); $product->set_gallery_image_ids( [] ); - } - - return $product; - } - - public static function get_attribute( $id ) { - global $wpdb; + } - $attribute = $wpdb->get_row( + return $product; + } + + /** + * Retrieve product attribute + * + * @param int $id Attribute ID. + * + * @throws \GraphQL\Error\UserError If attribute ID is invalid. + * + * @return object{'attribute_id': int} + */ + public static function get_attribute( $id ) { + global $wpdb; + + $attribute = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prepare( " SELECT * @@ -325,5 +387,5 @@ public static function get_attribute( $id ) { } return $attribute; - } -} \ No newline at end of file + } +} diff --git a/includes/model/class-product.php b/includes/model/class-product.php index 09ccd4e9..dbedb888 100644 --- a/includes/model/class-product.php +++ b/includes/model/class-product.php @@ -92,6 +92,7 @@ * * @property int[] $variation_ids * + * @mixin \WC_Product * @package WPGraphQL\WooCommerce\Model */ class Product extends WC_Post { diff --git a/includes/mutation/class-product-attribute-create.php b/includes/mutation/class-product-attribute-create.php index a3d67faf..53becba8 100644 --- a/includes/mutation/class-product-attribute-create.php +++ b/includes/mutation/class-product-attribute-create.php @@ -14,7 +14,6 @@ use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; -use WPGraphQL\WooCommerce\Model\Product; /** * Class Product_Attribute_Create @@ -36,14 +35,14 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'name' => [ + 'name' => [ 'type' => [ 'non_null' => 'String' ], 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), ], @@ -63,17 +62,17 @@ public static function get_input_fields() { 'type' => 'Boolean', 'description' => __( 'Whether the attribute has archives.', 'wp-graphql-woocommerce' ), ], - ]; - } + ]; + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'attribute' => [ + 'attribute' => [ 'type' => 'ProductAttributeObject', 'resolve' => static function ( $payload ) { return $payload['attribute']; @@ -82,16 +81,18 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * * @return callable */ public static function mutate_and_get_payload() { return static function ( $input, AppContext $context, ResolveInfo $info ) { - global $wpdb; + if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to create attributes.', 'wp-graphql-woocommerce' ) ); + } - $id = \wc_create_attribute( + $attribute_id = wc_create_attribute( [ 'name' => $input['name'], 'slug' => \wc_sanitize_taxonomy_name( stripslashes( $input['slug'] ) ), @@ -102,27 +103,23 @@ public static function mutate_and_get_payload() { ); // Checks for errors. - if ( is_wp_error( $id ) ) { - throw new UserError( $id->get_error_message() ); + if ( is_wp_error( $attribute_id ) ) { + throw new UserError( $attribute_id->get_error_message() ); } - $attribute = Product_Mutation::get_attribute( $id ); - - if ( is_wp_error( $attribute ) ) { - throw new UserError( $attribute->get_error_message() ); - } + $attribute = Product_Mutation::get_attribute( $attribute_id ); /** * Fires after a single product attribute is created or updated via the REST API. * - * @param stdObject $attribute Inserted attribute object. - * @param array $input Request object. - * @param boolean $creating True when creating attribute, false when updating. + * @param object{'attribute_id': int} $attribute Inserted attribute object. + * @param array $input Request object. + * @param boolean $creating True when creating attribute, false when updating. */ do_action( 'graphql_woocommerce_insert_product_attribute', $attribute, $input, true ); - return [ 'attribute' => $attribute ]; - }; - } + return [ 'attribute' => $attribute ]; + }; + } } diff --git a/includes/mutation/class-product-attribute-delete.php b/includes/mutation/class-product-attribute-delete.php index 405b7e6a..a1fbcdca 100644 --- a/includes/mutation/class-product-attribute-delete.php +++ b/includes/mutation/class-product-attribute-delete.php @@ -14,7 +14,6 @@ use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; -use WPGraphQL\WooCommerce\Model\Product; /** * Class Product_Attribute_Delete @@ -36,28 +35,28 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), - ], - ]; - } + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + ]; + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'attribute' => [ + 'attribute' => [ 'type' => 'ProductAttributeObject', 'resolve' => static function ( $payload ) { return $payload['attribute']; @@ -66,20 +65,19 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * * @return callable */ public static function mutate_and_get_payload() { return static function ( $input, AppContext $context, ResolveInfo $info ) { - $attribute = Product_Mutation::get_attribute( $input['id'] ); - - if ( is_wp_error( $attribute ) ) { - return $attribute; + if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { + throw new UserError( __( 'Sorry, you cannot delete attributes.', 'wp-graphql-woocommerce' ) ); } - $deleted = \wc_delete_attribute( $attribute->attribute_id ); + $attribute = Product_Mutation::get_attribute( $input['id'] ); + $deleted = wc_delete_attribute( $attribute->attribute_id ); if ( false === $deleted ) { throw new UserError( __( 'Failed to delete attribute.', 'wp-graphql-woocommerce' ) ); @@ -88,11 +86,11 @@ public static function mutate_and_get_payload() { /** * Fires after a single attribute is deleted via the REST API. * - * @param stdObject $attribute The deleted attribute. + * @param object{attribute_id: int} $attribute The deleted attribute. */ do_action( 'graphql_woocommerce_delete_product_attribute', $attribute ); - return [ 'attribute' => $attribute ]; - }; - } + return [ 'attribute' => $attribute ]; + }; + } } diff --git a/includes/mutation/class-product-attribute-term-create.php b/includes/mutation/class-product-attribute-term-create.php index 062103af..82f6e968 100644 --- a/includes/mutation/class-product-attribute-term-create.php +++ b/includes/mutation/class-product-attribute-term-create.php @@ -13,8 +13,6 @@ use GraphQL\Error\UserError; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; -use WPGraphQL\WooCommerce\Model\Product; /** * Class Product_Attribute_Term_Create @@ -36,44 +34,44 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'attributeId' => [ + 'attributeId' => [ 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'The ID of the attribute to which the term belongs.', 'wp-graphql' ), + 'description' => __( 'The ID of the attribute to which the term belongs.', 'wp-graphql-woocommerce' ), ], 'name' => [ 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'The name of the term.', 'wp-graphql' ), + 'description' => __( 'The name of the term.', 'wp-graphql-woocommerce' ), ], 'slug' => [ 'type' => 'String', - 'description' => __( 'The slug of the term.', 'wp-graphql' ), + 'description' => __( 'The slug of the term.', 'wp-graphql-woocommerce' ), ], 'description' => [ 'type' => 'String', - 'description' => __( 'The description of the term.', 'wp-graphql' ), + 'description' => __( 'The description of the term.', 'wp-graphql-woocommerce' ), ], 'menuOrder' => [ 'type' => 'Int', - 'description' => __( 'The order of the term in the menu.', 'wp-graphql' ), + 'description' => __( 'The order of the term in the menu.', 'wp-graphql-woocommerce' ), ], - ]; - } + ]; + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'term' => [ + 'term' => [ 'type' => 'ProductAttributeTermObject', 'resolve' => static function ( $payload ) { return (object) $payload['term']; @@ -82,19 +80,35 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * - * @return callable + * @param array $input Mutation input. + * @param \WPGraphQL\AppContext $context AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. Can be + * use to get info about the current node in the GraphQL tree. + * + * @throws \GraphQL\Error\UserError Invalid ID provided | Lack of capabilities. + * + * @return array */ public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { if ( ! $input['attributeId'] ) { - throw new UserError( __( 'An attributeId is required to create a new product attribute term.', 'wp-graphql' ) ); + throw new UserError( __( 'An attributeId is required to create a new product attribute term.', 'wp-graphql-woocommerce' ) ); + } + + $context = 'createProductAttributeTerm' === $info->fieldName ? 'create' : 'edit'; + $taxonomy = wc_attribute_taxonomy_name_by_id( $input['attributeId'] ); + if ( empty( $taxonomy ) ) { + throw new UserError( __( 'Invalid attributeId.', 'wp-graphql-woocommerce' ) ); + } + + if ( ! wc_rest_check_product_term_permissions( $taxonomy, $context ) ) { + throw new UserError( __( 'Sorry, you are not allowed to create product attribute terms.', 'wp-graphql-woocommerce' ) ); } - $taxonomy = \wc_attribute_taxonomy_name_by_id( $input['attributeId'] ); - $id = isset( $input['id'] ) ? $input['id'] : null; - $args = []; + $id = isset( $input['id'] ) ? $input['id'] : null; + $args = []; if ( ! empty( $input['description'] ) ) { $args['description'] = $input['description']; @@ -108,10 +122,19 @@ public static function mutate_and_get_payload( $input, AppContext $context, Reso $args['name'] = $input['name']; } - if ( $id && ! empty( $args ) ) { - $term = wp_update_term( $id, $taxonomy, $args ); - } elseif ( $id && isset( $input['menuOrder'] ) ) { + $term = null; + if ( $id ) { $term = get_term( $id, $taxonomy ); + } + + if ( is_wp_error( $term ) ) { + throw new UserError( $term->get_error_message() ); + } elseif ( $term && ! wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ) ) { + throw new UserError( __( 'Sorry, you are not allowed to update this product attribute term.', 'wp-graphql-woocommerce' ) ); + } + + if ( $id ) { + $term = wp_update_term( $id, $taxonomy, $args ); } elseif ( ! empty( $input['name'] ) ) { $name = $input['name']; $term = wp_insert_term( $name, $taxonomy, $args ); @@ -119,8 +142,8 @@ public static function mutate_and_get_payload( $input, AppContext $context, Reso $updating = 'updateProductAttributeTerm' === $info->fieldName; throw new UserError( $updating - ? __( 'A name is required to create a new product attribute term.', 'wp-graphql' ) - : __( 'A valid term "id" and changeable parameter are required to update a product attribute term.', 'wp-graphql' ) + ? __( 'A name is required to create a new product attribute term.', 'wp-graphql-woocommerce' ) + : __( 'A valid term "id" and changeable parameter are required to update a product attribute term.', 'wp-graphql-woocommerce' ) ); } @@ -128,14 +151,27 @@ public static function mutate_and_get_payload( $input, AppContext $context, Reso throw new UserError( $term->get_error_message() ); } + /** + * Newly created product attribute term. + * + * @var \WP_Term|\WP_Error|null $term + */ $term = get_term( $term['term_id'], $taxonomy ); + if ( ! $term ) { + throw new UserError( __( 'Failed to retrieve term for modification. Please check input.', 'wp-graphql-woocommerce' ) ); + } elseif ( is_wp_error( $term ) ) { + throw new UserError( $term->get_error_message() ); + } if ( isset( $input['menuOrder'] ) ) { - update_term_meta( $term->term_id, 'order_' .$taxonomy, $input['menuOrder'] ); + $success = update_term_meta( $term->term_id, 'order_' . $taxonomy, $input['menuOrder'] ); + if ( is_wp_error( $success ) ) { + throw new UserError( $success->get_error_message() ); + } } $menu_order = get_term_meta( $term->term_id, 'order_' . $taxonomy, true ); - $data = [ + $data = [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, @@ -145,5 +181,5 @@ public static function mutate_and_get_payload( $input, AppContext $context, Reso ]; return [ 'term' => $data ]; - } + } } diff --git a/includes/mutation/class-product-attribute-term-delete.php b/includes/mutation/class-product-attribute-term-delete.php index 4b432c80..b85efc14 100644 --- a/includes/mutation/class-product-attribute-term-delete.php +++ b/includes/mutation/class-product-attribute-term-delete.php @@ -13,7 +13,6 @@ use GraphQL\Error\UserError; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; /** * Class Product_Attribute_Term_Delete @@ -35,32 +34,32 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'attributeId' => [ + 'attributeId' => [ 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'The ID of the attribute to which the term belongs.', 'wp-graphql' ), + 'description' => __( 'The ID of the attribute to which the term belongs.', 'wp-graphql-woocommerce' ), ], 'id' => [ 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'The ID of the term to update.', 'wp-graphql' ), + 'description' => __( 'The ID of the term to update.', 'wp-graphql-woocommerce' ), ], - ]; - } + ]; + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'term' => [ + 'term' => [ 'type' => 'ProductAttributeTermObject', 'resolve' => static function ( $payload ) { return (object) $payload['term']; @@ -69,7 +68,7 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * * @return callable @@ -77,43 +76,54 @@ public static function get_output_fields() { public static function mutate_and_get_payload() { return static function ( $input, AppContext $context, ResolveInfo $info ) { if ( ! $input['attributeId'] ) { - throw new UserError( __( 'A valid attributeId is required to create a new product attribute term.', 'wp-graphql' ) ); + throw new UserError( __( 'A valid attributeId is required to create a new product attribute term.', 'wp-graphql-woocommerce' ) ); } - $taxonomy = \wc_attribute_taxonomy_name_by_id( $input['attributeId'] ); + $taxonomy = wc_attribute_taxonomy_name_by_id( $input['attributeId'] ); + if ( empty( $taxonomy ) ) { + throw new UserError( __( 'Invalid attribute ID.', 'wp-graphql-woocommerce' ) ); + } if ( ! $input['id'] ) { - throw new UserError( __( 'A valid term ID is required to delete a product attribute term.', 'wp-graphql' ) ); + throw new UserError( __( 'A valid term ID is required to delete a product attribute term.', 'wp-graphql-woocommerce' ) ); } $term = get_term( $input['id'], $taxonomy ); - if ( is_wp_error( $term ) ) { - throw new UserError( __( 'Invalid term ID.', 'wp-graphql' ) ); + if ( ! $term ) { + throw new UserError( __( 'Invalid term ID.', 'wp-graphql-woocommerce' ) ); + } elseif ( is_wp_error( $term ) ) { + throw new UserError( $term->get_error_message() ); + } + + if ( ! wc_rest_check_product_term_permissions( $taxonomy, 'delete', $term->term_id ) ) { + throw new UserError( __( 'You do not have permission to delete this term.', 'wp-graphql-woocommerce' ) ); } + $menu_order = get_term_meta( $term->term_id, 'order_' . $taxonomy, true ); + $data = [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'description' => $term->description, - 'menuOrder' => $term->menu_order, - 'count' => $term->count, + 'menu_order' => ! empty( $menu_order ) ? absint( $menu_order ) : 0, + 'count' => absint( $term->count ), ]; $retval = wp_delete_term( $term->term_id, $term->taxonomy ); if ( ! $retval ) { - throw new UserError( __( 'Failed to delete term.', 'wp-graphql' ) ); + throw new UserError( __( 'Failed to delete term.', 'wp-graphql-woocommerce' ) ); } /** * Fires after a single term is deleted via the REST API. * - * @param WP_Term $term The deleted term. - * @param array $input Mutation input. + * @param \WP_Term $term The deleted term. + * @param array $input Mutation input. */ do_action( "graphql_woocommerce_delete_{$taxonomy}", $term, $input ); return [ 'term' => $data ]; }; - } + } } diff --git a/includes/mutation/class-product-attribute-term-update.php b/includes/mutation/class-product-attribute-term-update.php index 10920433..270a2785 100644 --- a/includes/mutation/class-product-attribute-term-update.php +++ b/includes/mutation/class-product-attribute-term-update.php @@ -10,11 +10,6 @@ namespace WPGraphQL\WooCommerce\Mutation; -use GraphQL\Error\UserError; -use GraphQL\Type\Definition\ResolveInfo; -use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; - /** * Class Product_Attribute_Term_Update */ @@ -35,7 +30,7 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array @@ -46,24 +41,24 @@ public static function get_input_fields() { [ 'id' => [ 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'The ID of the term to update.', 'wp-graphql' ), + 'description' => __( 'The ID of the term to update.', 'wp-graphql-woocommerce' ), ], 'name' => [ 'type' => 'String', - 'description' => __( 'The name of the term.', 'wp-graphql' ), + 'description' => __( 'The name of the term.', 'wp-graphql-woocommerce' ), ], ] ); - } + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'term' => [ + 'term' => [ 'type' => 'ProductAttributeTermObject', 'resolve' => static function ( $payload ) { return (object) $payload['term']; diff --git a/includes/mutation/class-product-attribute-update.php b/includes/mutation/class-product-attribute-update.php index 060f23fc..d32f7f75 100644 --- a/includes/mutation/class-product-attribute-update.php +++ b/includes/mutation/class-product-attribute-update.php @@ -14,7 +14,6 @@ use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; -use WPGraphQL\WooCommerce\Model\Product; /** * Class Product_Attribute_Update @@ -36,7 +35,7 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array @@ -51,16 +50,16 @@ public static function get_input_fields() { ], Product_Attribute_Create::get_input_fields() ); - } + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'attribute' => [ + 'attribute' => [ 'type' => 'ProductAttributeObject', 'resolve' => static function ( $payload ) { return $payload['attribute']; @@ -69,7 +68,7 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * * @return callable @@ -78,6 +77,10 @@ public static function mutate_and_get_payload() { return static function ( $input, AppContext $context, ResolveInfo $info ) { global $wpdb; + if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) { + throw new UserError( __( 'Sorry, you are not allowed to edit attributes.', 'wp-graphql-woocommerce' ) ); + } + $id = (int) $input['id']; $edited = \wc_update_attribute( $id, @@ -91,27 +94,22 @@ public static function mutate_and_get_payload() { ); // Checks for errors. - if ( is_wp_error( $id ) ) { - throw new UserError( $id->get_error_message() ); + if ( is_wp_error( $edited ) ) { + throw new UserError( $edited->get_error_message() ); } $attribute = Product_Mutation::get_attribute( $id ); - if ( is_wp_error( $attribute ) ) { - return [ 'attribute' => $attribute ]; - } - /** * Fires after a single product attribute is created or updated via the REST API. * - * @param stdObject $attribute Inserted attribute object. - * @param array $input Request object. - * @param boolean $creating True when creating attribute, false when updating. + * @param object{'attribute_id': int} $attribute Inserted attribute object. + * @param array $input Request object. + * @param boolean $creating True when creating attribute, false when updating. */ do_action( 'graphql_woocommerce_insert_product_attribute', $attribute, $input, false ); - - return [ 'attribute' => $attribute ]; - }; - } + return [ 'attribute' => $attribute ]; + }; + } } diff --git a/includes/mutation/class-product-create.php b/includes/mutation/class-product-create.php index 3768b79d..9df3ea59 100644 --- a/includes/mutation/class-product-create.php +++ b/includes/mutation/class-product-create.php @@ -36,193 +36,189 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'name' => [ - 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'Name of the product.', 'wp-graphql-woocommerce' ), - ], - 'slug' => [ - 'type' => 'String', - 'description' => __( 'Product slug.', 'wp-graphql-woocommerce' ), - ], - 'type' => [ - 'type' => 'ProductTypesEnum', - 'description' => __( 'Type of the product.', 'wp-graphql-woocommerce' ), - ], - 'status' => [ - 'type' => 'PostStatusEnum', - 'description' => __( 'Status of the product.', 'wp-graphql-woocommerce' ), - ], - 'featured' => [ - 'type' => 'Boolean', - 'description' => __( 'Featured product.', 'wp-graphql-woocommerce' ), - ], - 'catalogVisibility' => [ - 'type' => 'CatalogVisibilityEnum', - 'description' => __( 'Catalog visibility.', 'wp-graphql-woocommerce' ), - ], - 'description' => [ - 'type' => 'String', - 'description' => __( 'Product description.', 'wp-graphql-woocommerce' ), - ], - 'shortDescription' => [ - 'type' => 'String', - 'description' => __( 'Product short description.', 'wp-graphql-woocommerce' ), - ], - 'sku' => [ - 'type' => 'String', - 'description' => __( 'Product SKU.', 'wp-graphql-woocommerce' ), - ], - 'regularPrice' => [ - 'type' => 'Float', - 'description' => __( 'Product regular price.', 'wp-graphql-woocommerce' ), - ], - 'salePrice' => [ - 'type' => 'Float', - 'description' => __( 'Product sale price.', 'wp-graphql-woocommerce' ), - ], - 'dateOnSaleFrom' => [ - 'type' => 'String', - 'description' => __( 'Product sale start date.', 'wp-graphql-woocommerce' ), - ], - 'dateOnSaleTo' => [ - 'type' => 'String', - 'description' => __( 'Product sale end date.', 'wp-graphql-woocommerce' ), - ], - 'virtual' => [ - 'type' => 'Boolean', - 'description' => __( 'Product virtual.', 'wp-graphql-woocommerce' ), - ], - 'downloadable' => [ - 'type' => 'Boolean', - 'description' => __( 'Product downloadable.', 'wp-graphql-woocommerce' ), - ], - 'downloads' => [ - 'type' => [ 'list_of' => 'ProductDownloadInput' ], - 'description' => __( 'Product downloads.', 'wp-graphql-woocommerce' ), - ], - 'downloadLimit' => [ - 'type' => 'Int', - 'description' => __( 'Product download limit.', 'wp-graphql-woocommerce' ), - ], - 'downloadExpiry' => [ - 'type' => 'Int', - 'description' => __( 'Number of days until download access expires.', 'wp-graphql-woocommerce' ), - ], - 'externalUrl' => [ - 'type' => 'String', - 'description' => __( 'Product external URL. (External products only)', 'wp-graphql-woocommerce' ), - ], - 'buttonText' => [ - 'type' => 'String', - 'description' => __( 'Product button text. (External products only)', 'wp-graphql-woocommerce' ), - ], - 'taxStatus' => [ - 'type' => 'TaxStatusEnum', - 'description' => __( 'Tax status.', 'wp-graphql-woocommerce' ), - ], - 'taxClass' => [ - 'type' => 'String', - 'description' => __( 'Tax class.', 'wp-graphql-woocommerce' ), - ], - 'manageStock' => [ - 'type' => 'Boolean', - 'description' => __( 'Manage stock.', 'wp-graphql-woocommerce' ), - ], - 'stockQuantity' => [ - 'type' => 'Int', - 'description' => __( 'Stock quantity.', 'wp-graphql-woocommerce' ), - ], - 'stockStatus' => [ - 'type' => 'StockStatusEnum', - 'description' => __( 'Stock status.', 'wp-graphql-woocommerce' ), - ], - 'backorders' => [ - 'type' => 'BackordersEnum', - 'description' => __( 'Backorders.', 'wp-graphql-woocommerce' ), - ], - 'soldIndividually' => [ - 'type' => 'Boolean', - 'description' => __( 'Sold individually.', 'wp-graphql-woocommerce' ), - ], - 'weight' => [ - 'type' => 'String', - 'description' => __( 'Product weight.', 'wp-graphql-woocommerce' ), - ], - 'dimensions' => [ - 'type' => 'ProductDimensionsInput', - 'description' => __( 'Product dimensions.', 'wp-graphql-woocommerce' ), - ], - 'shippingClass' => [ - 'type' => 'String', - 'description' => __( 'Shipping class.', 'wp-graphql-woocommerce' ), - ], - 'reviewsAllowed' => [ - 'type' => 'Boolean', - 'description' => __( 'Allow reviews. Default is true', 'wp-graphql-woocommerce' ), - ], - 'upsellIds' => [ - 'type' => [ 'list_of' => 'Int' ], - 'description' => __( 'Upsell product IDs.', 'wp-graphql-woocommerce' ), - ], - 'crossSellIds' => [ - 'type' => [ 'list_of' => 'Int' ], - 'description' => __( 'Cross-sell product IDs.', 'wp-graphql-woocommerce' ), - ], - 'parentId' => [ - 'type' => 'Int', - 'description' => __( 'Parent product ID.', 'wp-graphql-woocommerce' ), - ], - 'purchaseNote' => [ - 'type' => 'String', - 'description' => __( 'Purchase note.', 'wp-graphql-woocommerce' ), - ], - 'categories' => [ - 'type' => [ 'list_of' => 'Int' ], - 'description' => __( 'Product categories.', 'wp-graphql-woocommerce' ), - ], - 'tags' => [ - 'type' => [ 'list_of' => 'Int' ], - 'description' => __( 'Product tags.', 'wp-graphql-woocommerce' ), - ], - 'images' => [ - 'type' => [ 'list_of' => 'ProductImageInput' ], - 'description' => __( 'Product images.', 'wp-graphql-woocommerce' ), - ], - 'attributes' => [ - 'type' => [ 'list_of' => 'ProductAttributesInput' ], - 'description' => __( 'Product attributes.', 'wp-graphql-woocommerce' ), - ], - 'defaultAttributes' => [ - 'type' => [ 'list_of' => 'ProductAttributeInput' ], - 'description' => __( 'Product default attributes.', 'wp-graphql-woocommerce' ), - ], - 'variations' => [ - 'type' => [ 'list_of' => 'ProductVariationInput' ], - 'description' => __( 'Product variations.', 'wp-graphql-woocommerce' ), - ], - 'groupedProducts' => [ - 'type' => [ 'list_of' => 'Int' ], - 'description' => __( 'Grouped product IDs.', 'wp-graphql-woocommerce' ), - ], - 'menuOrder' => [ - 'type' => 'Int', - 'description' => __( 'Menu order.', 'wp-graphql-woocommerce' ), - ], - 'metaData' => [ - 'type' => [ 'list_of' => 'MetaDataInput' ], - 'description' => __( 'Meta data.', 'wp-graphql-woocommerce' ), - ], - ]; - } - - /** + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Name of the product.', 'wp-graphql-woocommerce' ), + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Product slug.', 'wp-graphql-woocommerce' ), + ], + 'type' => [ + 'type' => 'ProductTypesEnum', + 'description' => __( 'Type of the product.', 'wp-graphql-woocommerce' ), + ], + 'status' => [ + 'type' => 'PostStatusEnum', + 'description' => __( 'Status of the product.', 'wp-graphql-woocommerce' ), + ], + 'featured' => [ + 'type' => 'Boolean', + 'description' => __( 'Featured product.', 'wp-graphql-woocommerce' ), + ], + 'catalogVisibility' => [ + 'type' => 'CatalogVisibilityEnum', + 'description' => __( 'Catalog visibility.', 'wp-graphql-woocommerce' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Product description.', 'wp-graphql-woocommerce' ), + ], + 'shortDescription' => [ + 'type' => 'String', + 'description' => __( 'Product short description.', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Product SKU.', 'wp-graphql-woocommerce' ), + ], + 'regularPrice' => [ + 'type' => 'Float', + 'description' => __( 'Product regular price.', 'wp-graphql-woocommerce' ), + ], + 'salePrice' => [ + 'type' => 'Float', + 'description' => __( 'Product sale price.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleFrom' => [ + 'type' => 'String', + 'description' => __( 'Product sale start date.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleTo' => [ + 'type' => 'String', + 'description' => __( 'Product sale end date.', 'wp-graphql-woocommerce' ), + ], + 'virtual' => [ + 'type' => 'Boolean', + 'description' => __( 'Product virtual.', 'wp-graphql-woocommerce' ), + ], + 'downloadable' => [ + 'type' => 'Boolean', + 'description' => __( 'Product downloadable.', 'wp-graphql-woocommerce' ), + ], + 'downloads' => [ + 'type' => [ 'list_of' => 'ProductDownloadInput' ], + 'description' => __( 'Product downloads.', 'wp-graphql-woocommerce' ), + ], + 'downloadLimit' => [ + 'type' => 'Int', + 'description' => __( 'Product download limit.', 'wp-graphql-woocommerce' ), + ], + 'downloadExpiry' => [ + 'type' => 'Int', + 'description' => __( 'Number of days until download access expires.', 'wp-graphql-woocommerce' ), + ], + 'externalUrl' => [ + 'type' => 'String', + 'description' => __( 'Product external URL. (External products only)', 'wp-graphql-woocommerce' ), + ], + 'buttonText' => [ + 'type' => 'String', + 'description' => __( 'Product button text. (External products only)', 'wp-graphql-woocommerce' ), + ], + 'taxStatus' => [ + 'type' => 'TaxStatusEnum', + 'description' => __( 'Tax status.', 'wp-graphql-woocommerce' ), + ], + 'taxClass' => [ + 'type' => 'TaxClassEnum', + 'description' => __( 'Tax class.', 'wp-graphql-woocommerce' ), + ], + 'manageStock' => [ + 'type' => 'Boolean', + 'description' => __( 'Manage stock.', 'wp-graphql-woocommerce' ), + ], + 'stockQuantity' => [ + 'type' => 'Int', + 'description' => __( 'Stock quantity.', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => 'StockStatusEnum', + 'description' => __( 'Stock status.', 'wp-graphql-woocommerce' ), + ], + 'backorders' => [ + 'type' => 'BackordersEnum', + 'description' => __( 'Backorders.', 'wp-graphql-woocommerce' ), + ], + 'soldIndividually' => [ + 'type' => 'Boolean', + 'description' => __( 'Sold individually.', 'wp-graphql-woocommerce' ), + ], + 'weight' => [ + 'type' => 'String', + 'description' => __( 'Product weight.', 'wp-graphql-woocommerce' ), + ], + 'dimensions' => [ + 'type' => 'ProductDimensionsInput', + 'description' => __( 'Product dimensions.', 'wp-graphql-woocommerce' ), + ], + 'shippingClass' => [ + 'type' => 'String', + 'description' => __( 'Shipping class.', 'wp-graphql-woocommerce' ), + ], + 'reviewsAllowed' => [ + 'type' => 'Boolean', + 'description' => __( 'Allow reviews. Default is true', 'wp-graphql-woocommerce' ), + ], + 'upsellIds' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Upsell product IDs.', 'wp-graphql-woocommerce' ), + ], + 'crossSellIds' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Cross-sell product IDs.', 'wp-graphql-woocommerce' ), + ], + 'parentId' => [ + 'type' => 'Int', + 'description' => __( 'Parent product ID.', 'wp-graphql-woocommerce' ), + ], + 'purchaseNote' => [ + 'type' => 'String', + 'description' => __( 'Purchase note.', 'wp-graphql-woocommerce' ), + ], + 'categories' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Product categories.', 'wp-graphql-woocommerce' ), + ], + 'tags' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Product tags.', 'wp-graphql-woocommerce' ), + ], + 'images' => [ + 'type' => [ 'list_of' => 'ProductImageInput' ], + 'description' => __( 'Product images.', 'wp-graphql-woocommerce' ), + ], + 'attributes' => [ + 'type' => [ 'list_of' => 'ProductAttributesInput' ], + 'description' => __( 'Product attributes.', 'wp-graphql-woocommerce' ), + ], + 'defaultAttributes' => [ + 'type' => [ 'list_of' => 'ProductAttributeInput' ], + 'description' => __( 'Product default attributes.', 'wp-graphql-woocommerce' ), + ], + 'groupedProducts' => [ + 'type' => [ 'list_of' => 'Int' ], + 'description' => __( 'Grouped product IDs.', 'wp-graphql-woocommerce' ), + ], + 'menuOrder' => [ + 'type' => 'Int', + 'description' => __( 'Menu order.', 'wp-graphql-woocommerce' ), + ], + 'metaData' => [ + 'type' => [ 'list_of' => 'MetaDataInput' ], + 'description' => __( 'Meta data.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** * Defines the mutation output field configuration * * @return array @@ -244,259 +240,295 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * - * @return callable + * @param array $input Mutation input. + * @param \WPGraphQL\AppContext $context AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. Can be + * use to get info about the current node in the GraphQL tree. + * + * @throws \GraphQL\Error\UserError Invalid ID provided | Lack of capabilities. + * + * @return array */ public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { - $product_id = ! empty( $input['id'] ) ? $input['id'] : 0; - $type = ! empty( $input['type'] ) ? $input['type'] : 'simple'; - - if ( 0 !== $product_id ) { - $product = \wc_get_product( $product_id ); - if ( $product && ! wc_rest_check_post_permissions( 'product', 'edit', $product->get_id() ) ) { - throw new UserError( __( 'You do not have permission to edit this product', 'wp-graphql-woocommerce' ) ); - } - } else { - $classname = \WC_Product_Factory::get_classname_from_product_type( $type ); - if ( ! class_exists( $classname ) ) { - $classname = '\WC_Product_Simple'; - } - - $product = new $classname( $product_id ); - - $post_type_object = get_post_type_object( 'product' ); - if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) { - throw new UserError( __( 'You do not have permission to create products', 'wp-graphql-woocommerce' ) ); - } - } - - if ( ! empty( $input['name'] ) ) { - $product->set_name( wp_filter_post_kses( $input['name'] ) ); - } - - if ( ! empty( $input['description'] ) ) { - $product->set_description( wp_filter_post_kses( $input['description'] ) ); - } - - if ( ! empty( $input['shortDescription'] ) ) { - $product->set_short_description( wp_filter_post_kses( $input['shortDescription'] ) ); - } - - if ( ! empty( $input['status'] ) ) { - $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); - } - - if ( ! empty( $input['slug'] ) ) { - $product->set_slug( $input['slug'] ); - } - - if ( ! empty( $input['menuOrder'] ) ) { - $product->set_menu_order( $input['menuOrder'] ); - } - - if ( isset( $input['reviewsAllowed'] ) ) { - $product->set_reviews_allowed( $input['reviewsAllowed'] ); - } - - if ( isset( $input['virtual'] ) ) { - $product->set_virtual( $input['virtual'] ); - } - - if ( ! empty( $input['taxStatus'] ) ) { - $product->set_tax_status( $input['taxStatus'] ); - } - - if ( ! empty( $input['taxClass'] ) ) { - $product->set_tax_class( $input['taxClass'] ); - } - - if ( ! empty( $input['catalogVisibility'] ) ) { - $product->set_catalog_visibility( $input['catalogVisibility'] ); - } - - if ( ! empty( $input['purchaseNote'] ) ) { - $product->set_purchase_note( wp_filter_post_kses( $input['purchaseNote'] ) ); - } - - if ( isset( $input['featured'] ) ) { - $product->set_featured( $input['featured'] ); - } - - $product = Product_Mutation::save_product_shipping_data( $product, $input ); - - if ( ! empty( $input['sku'] ) ) { - $product->set_sku( wc_clean( $input['sku'] ) ); - } - - if ( ! empty( $input['attributes'] ) ) { - $attributes = []; - - foreach ( $input['attributes'] as $attribute ) { - $attribute_object = Product_Mutation::prepare_attribute( $attribute ); - if ( $attribute_object ) { - $attributes[] = $attribute_object; - } - } - - $product->set_attributes( $attributes ); - } - - if ( in_array( $type, [ 'variable', 'grouped' ], true ) ) { - $product->set_regular_price( '' ); - $product->set_sale_price( '' ); - $product->set_date_on_sale_to( '' ); - $product->set_date_on_sale_from( '' ); - $product->set_price( '' ); - } else { - if ( ! empty( $input['regularPrice'] ) ) { - $product->set_regular_price( $input['regularPrice'] ); - } - - if ( ! empty( $input['salePrice'] ) ) { - $product->set_sale_price( $input['salePrice'] ); - } - - if ( ! empty( $input['dateOnSaleFrom'] ) ) { - $product->set_date_on_sale_from( $input['dateOnSaleFrom'] ); - } - - if ( ! empty( $input['dateOnSaleTo'] ) ) { - $product->set_date_on_sale_to( $input['dateOnSaleTo'] ); - } - } - - if ( ! empty( $input['parentId'] ) ) { - $product->set_parent_id( $input['parentId'] ); - } - - if ( isset( $input['soldIndividually'] ) ) { - $product->set_sold_individually( $input['soldIndividually'] ); - } - - if ( isset( $input['stockStatus'] ) ) { - $stock_status = wc_clean( $input['stockStatus'] ); - } else { - $stock_status = $product->get_stock_status(); - } - - if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { - if ( isset( $input['manageStock'] ) ) { - $product->set_manage_stock( $input['manageStock'] ); - } - - - if ( isset( $input['backorders'] ) ) { - $product->set_backorders( $input['backorders'] ); - } - - if ( $product->is_type( 'grouped' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } elseif ( $product->is_type( 'external' ) ) { - $product->set_manage_stock( 'no' ); - $product->set_backorders( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( 'instock' ); - } elseif ( $product->get_manage_stock() ) { - // Stock status is always determined by children so sync later. - if ( ! $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - // Stock quantity. - if ( isset( $input['stockQuantity'] ) ) { - $product->set_stock_quantity( wc_stock_amount( $input['stockQuantity'] ) ); - } - } else { - // Don't manage stock. - $product->set_manage_stock( 'no' ); - $product->set_stock_quantity( '' ); - $product->set_stock_status( $stock_status ); - } - } elseif ( $product->is_type( 'variable' ) ) { - $product->set_stock_status( $stock_status ); - } - - if ( ! empty( $input['upsellIds'] ) ) { - $product->set_upsell_ids( $input['upsellIds'] ); - } - - if ( ! empty( $input['crossSellIds'] ) ) { - $product->set_cross_sell_ids( $input['crossSellIds'] ); - } - - if ( ! empty( $input['categories'] ) ) { - $product = Product_Mutation::save_taxonomy_terms( $product, $input['categories'] ); - } - - if ( ! empty( $input['tags'] ) ) { - $product = Product_Mutation::save_taxonomy_terms( $product, $input['tags'], 'tag' ); - } - - if ( isset( $input['downloadable'] ) ) { - $product->set_downloadable( $input['downloadable'] ); - } - - if ( $product->get_downloadable() ) { - if ( ! empty( $input['downloads'] ) ) { - $product = Product_Mutation::save_downloadable_files( $product, $input['downloads'] ); - } - - if ( isset( $input['downloadLimit'] ) ) { - $product->set_download_limit( $input['downloadLimit'] ); - } - - if ( isset( $input['downloadExpiry'] ) ) { - $product->set_download_expiry( $input['downloadExpiry'] ); - } - } - - if ( $product->is_type( 'external' ) ) { - if ( ! empty( $input['externalUrl'] ) ) { - $product->set_product_url( $input['externalUrl'] ); - } - - if ( ! empty( $input['buttonText'] ) ) { - $product->set_button_text( $input['buttonText'] ); - } - } - - if ( $product->is_type( 'variable' ) ) { - $product = Product_Mutation::save_default_attributes( $product, $input ); - } - - if ( $product->is_type( 'grouped' ) && isset( $input['groupedProducts'] ) ) { - $product->set_children( $input['groupedProducts'] ); - } - - if ( ! empty( $input['images'] ) ) { - $product = Product_Mutation::set_product_images( $product, $input['images'] ); - } - - if ( ! empty( $input['metaData'] ) ) { - foreach( $input['metaData'] as $meta ) { - $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } - - /** - * Filters an object before it is inserted via the GraphQL API. - * - * The dynamic portion of the hook name, `$this->post_type`, - * refers to the object type slug. - * - * @param WC_Data $product Object object. - * @param array $input GraphQL input object. - * @param bool $creating If is creating a new object. - */ - $product = apply_filters( 'graphql_woocommerce_pre_insert_product_object', $product, $input, true ); - - $product_id = $product->save(); - - return [ 'id' => $product_id ]; - } + $product_id = ! empty( $input['id'] ) ? $input['id'] : 0; + $type = ! empty( $input['type'] ) ? $input['type'] : 'simple'; + + if ( 0 !== $product_id ) { + /** + * @var \WC_Product $product + */ + $product = \wc_get_product( $product_id ); + if ( $product && ! wc_rest_check_post_permissions( 'product', 'edit', $product->get_id() ) ) { + throw new UserError( __( 'You do not have permission to edit this product', 'wp-graphql-woocommerce' ) ); + } + } else { + $classname = \WC_Product_Factory::get_classname_from_product_type( $type ); + if ( ! $classname || ! class_exists( $classname ) ) { + $classname = '\WC_Product_Simple'; + } + + /** + * @var \WC_Product $product + */ + $product = new $classname( $product_id ); + + /** + * @var \WP_Post_Type $post_type_object + */ + $post_type_object = get_post_type_object( 'product' ); + if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) { + throw new UserError( __( 'You do not have permission to create products', 'wp-graphql-woocommerce' ) ); + } + } + + if ( ! empty( $input['name'] ) ) { + $product->set_name( wp_filter_post_kses( $input['name'] ) ); + } + + if ( ! empty( $input['description'] ) ) { + $product->set_description( wp_filter_post_kses( $input['description'] ) ); + } + + if ( ! empty( $input['shortDescription'] ) ) { + $product->set_short_description( wp_filter_post_kses( $input['shortDescription'] ) ); + } + + if ( ! empty( $input['status'] ) ) { + $product->set_status( get_post_status_object( $input['status'] ) ? $input['status'] : 'draft' ); + } + + if ( ! empty( $input['slug'] ) ) { + $product->set_slug( $input['slug'] ); + } + + if ( ! empty( $input['menuOrder'] ) ) { + $product->set_menu_order( $input['menuOrder'] ); + } + + if ( isset( $input['reviewsAllowed'] ) ) { + $product->set_reviews_allowed( $input['reviewsAllowed'] ); + } + + if ( isset( $input['virtual'] ) ) { + $product->set_virtual( $input['virtual'] ); + } + + if ( ! empty( $input['taxStatus'] ) ) { + $product->set_tax_status( $input['taxStatus'] ); + } + + if ( ! empty( $input['taxClass'] ) ) { + $product->set_tax_class( $input['taxClass'] ); + } + + if ( ! empty( $input['catalogVisibility'] ) ) { + $product->set_catalog_visibility( $input['catalogVisibility'] ); + } + + if ( ! empty( $input['purchaseNote'] ) ) { + $product->set_purchase_note( wp_filter_post_kses( $input['purchaseNote'] ) ); + } + + if ( isset( $input['featured'] ) ) { + $product->set_featured( $input['featured'] ); + } + + $product = Product_Mutation::save_product_shipping_data( $product, $input ); + + if ( ! empty( $input['sku'] ) ) { + /** + * @var string $sku + */ + $sku = wc_clean( $input['sku'] ); + $product->set_sku( $sku ); + } + + if ( ! empty( $input['attributes'] ) ) { + $attributes = []; + + foreach ( $input['attributes'] as $attribute ) { + $attribute_object = Product_Mutation::prepare_attribute( $attribute ); + if ( $attribute_object ) { + $attributes[] = $attribute_object; + } + } + + $product->set_attributes( $attributes ); + } + + if ( in_array( $type, [ 'variable', 'grouped' ], true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + if ( ! empty( $input['regularPrice'] ) ) { + $product->set_regular_price( $input['regularPrice'] ); + } + + if ( ! empty( $input['salePrice'] ) ) { + $product->set_sale_price( $input['salePrice'] ); + } + + if ( ! empty( $input['dateOnSaleFrom'] ) ) { + $product->set_date_on_sale_from( $input['dateOnSaleFrom'] ); + } + + if ( ! empty( $input['dateOnSaleTo'] ) ) { + $product->set_date_on_sale_to( $input['dateOnSaleTo'] ); + } + } + + if ( ! empty( $input['parentId'] ) ) { + $product->set_parent_id( $input['parentId'] ); + } + + if ( isset( $input['soldIndividually'] ) ) { + $product->set_sold_individually( $input['soldIndividually'] ); + } + + if ( isset( $input['stockStatus'] ) ) { + /** + * @var string $stock_status + */ + $stock_status = wc_clean( $input['stockStatus'] ); + } else { + $stock_status = $product->get_stock_status(); + } + + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + if ( isset( $input['manageStock'] ) ) { + /** + * @var bool $manage_stock + */ + $manage_stock = $input['manageStock']; + $product->set_manage_stock( $manage_stock ); + } + + + if ( isset( $input['backorders'] ) ) { + $product->set_backorders( $input['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( false ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( null ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( false ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( null ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $input['stockQuantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $input['stockQuantity'] ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( false ); + $product->set_stock_quantity( null ); + $product->set_stock_status( $stock_status ); + } + } elseif ( $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + if ( ! empty( $input['upsellIds'] ) ) { + $product->set_upsell_ids( $input['upsellIds'] ); + } + + if ( ! empty( $input['crossSellIds'] ) ) { + $product->set_cross_sell_ids( $input['crossSellIds'] ); + } + + if ( ! empty( $input['categories'] ) ) { + $product = Product_Mutation::save_taxonomy_terms( $product, $input['categories'] ); + } + + if ( ! empty( $input['tags'] ) ) { + $product = Product_Mutation::save_taxonomy_terms( $product, $input['tags'], 'tag' ); + } + + if ( isset( $input['downloadable'] ) ) { + $product->set_downloadable( $input['downloadable'] ); + } + + if ( $product->get_downloadable() ) { + if ( ! empty( $input['downloads'] ) ) { + $product = Product_Mutation::save_downloadable_files( $product, $input['downloads'] ); + } + + if ( isset( $input['downloadLimit'] ) ) { + $product->set_download_limit( $input['downloadLimit'] ); + } + + if ( isset( $input['downloadExpiry'] ) ) { + $product->set_download_expiry( $input['downloadExpiry'] ); + } + } + + if ( $product->is_type( 'external' ) ) { + if ( ! empty( $input['externalUrl'] ) ) { + /** + * @var \WC_Product_External $product + */ + $product->set_product_url( $input['externalUrl'] ); + } + + if ( ! empty( $input['buttonText'] ) ) { + /** + * @var \WC_Product_External $product + */ + $product->set_button_text( $input['buttonText'] ); + } + } + + if ( $product->is_type( 'variable' ) ) { + $product = Product_Mutation::save_default_attributes( $product, $input ); + } + + if ( $product->is_type( 'grouped' ) && isset( $input['groupedProducts'] ) ) { + /** + * @var \WC_Product_Grouped $product + */ + $product->set_children( $input['groupedProducts'] ); + } + + if ( ! empty( $input['images'] ) ) { + $product = Product_Mutation::set_product_images( $product, $input['images'] ); + } + + if ( ! empty( $input['metaData'] ) ) { + foreach ( $input['metaData'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the GraphQL API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param \WC_Product $product Object object. + * @param array $input GraphQL input object. + * @param bool $creating If is creating a new object. + */ + $product = apply_filters( 'graphql_woocommerce_pre_insert_product_object', $product, $input, true ); + + $product_id = $product->save(); + + return [ 'id' => $product_id ]; + } } diff --git a/includes/mutation/class-product-delete.php b/includes/mutation/class-product-delete.php index 5207c1c0..70a8fe11 100644 --- a/includes/mutation/class-product-delete.php +++ b/includes/mutation/class-product-delete.php @@ -13,7 +13,6 @@ use GraphQL\Error\UserError; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; use WPGraphQL\WooCommerce\Model\Product; /** @@ -36,32 +35,32 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), - ], + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], 'force' => [ 'type' => 'Boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'wp-graphql-woocommerce' ), ], - ]; - } + ]; + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'product' => [ + 'product' => [ 'type' => 'Product', 'resolve' => static function ( $payload ) { return $payload['product']; @@ -70,7 +69,7 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * * @return callable @@ -82,7 +81,7 @@ public static function mutate_and_get_payload() { $object = new Product( $product_id ); $result = false; - if ( ! $object || 0 === $object->get_id() ) { + if ( 0 === $object->ID ) { throw new UserError( __( 'Invalid product ID.', 'wp-graphql-woocommerce' ) ); } @@ -97,16 +96,21 @@ public static function mutate_and_get_payload() { * * Return false to disable trash support for the object. * - * @param boolean $supports_trash Whether the object type support trashing. - * @param \WC_Product $object The object being considered for trashing support. + * @param boolean $supports_trash Whether the object type support trashing. + * @param \WPGraphQL\WooCommerce\Model\Product $object The object being considered for trashing support. */ - $supports_trash = apply_filters( "graphql_woocommerce_product_object_trashable", $supports_trash, $object ); + $supports_trash = apply_filters( 'graphql_woocommerce_product_object_trashable', $supports_trash, $object ); - if ( ! wc_rest_check_post_permissions( 'product', 'delete', $object->get_id() ) ) { + if ( ! wc_rest_check_post_permissions( 'product', 'delete', $object->ID ) ) { throw new UserError( __( 'Sorry, you are not allowed to delete products', 'wp-graphql-woocommerce' ) ); } - $product_to_be_deleted = \wc_get_product( $object->get_id() ); + /** + * Get the product to be deleted. + * + * @var \WC_Product $product_to_be_deleted + */ + $product_to_be_deleted = \wc_get_product( $object->ID ); if ( $force ) { if ( $product_to_be_deleted->is_type( 'variable' ) ) { @@ -135,13 +139,18 @@ public static function mutate_and_get_payload() { throw new UserError( __( 'This product does not support trashing.', 'wp-graphql-woocommerce' ) ); } - if ( is_callable( array( $product_to_be_deleted, 'get_status' ) ) ) { + if ( is_callable( [ $product_to_be_deleted, 'get_status' ] ) ) { if ( 'trash' === $product_to_be_deleted->get_status() ) { throw new UserError( __( 'Product is already in the trash.', 'wp-graphql-woocommerce' ) ); } $product_to_be_deleted->delete(); - $result = 'trash' === $product_to_be_deleted->get_status(); + + /** + * @var string $status + */ + $status = $product_to_be_deleted->get_status(); + $result = 'trash' === $status; } } @@ -156,12 +165,12 @@ public static function mutate_and_get_payload() { /** * Fires after a single object is deleted or trashed via the REST API. * - * @param Product $object The deleted or trashed object. + * @param \WPGraphQL\WooCommerce\Model\Product $object The deleted or trashed object. * @param array $input The mutation input. */ - do_action( "graphql_woocommerce_delete_product_object", $object, $input ); + do_action( 'graphql_woocommerce_delete_product_object', $object, $input ); - return [ 'product' => $object ]; - }; - } + return [ 'product' => $object ]; + }; + } } diff --git a/includes/mutation/class-product-update.php b/includes/mutation/class-product-update.php index a719b541..f36ddcfe 100644 --- a/includes/mutation/class-product-update.php +++ b/includes/mutation/class-product-update.php @@ -10,10 +10,6 @@ namespace WPGraphQL\WooCommerce\Mutation; -use GraphQL\Error\UserError; -use GraphQL\Type\Definition\ResolveInfo; -use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; use WPGraphQL\WooCommerce\Model\Product; /** @@ -36,7 +32,7 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array @@ -51,16 +47,16 @@ public static function get_input_fields() { ], Product_Create::get_input_fields() ); - } + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'product' => [ + 'product' => [ 'type' => 'Product', 'resolve' => static function ( $payload ) { return new Product( $payload['id'] ); diff --git a/includes/mutation/class-product-variation-create.php b/includes/mutation/class-product-variation-create.php index b070146b..20985d45 100644 --- a/includes/mutation/class-product-variation-create.php +++ b/includes/mutation/class-product-variation-create.php @@ -36,128 +36,128 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'productId' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), - ], - 'description' => [ - 'type' => 'String', - 'description' => __( 'Description of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'sku' => [ - 'type' => 'String', - 'description' => __( 'Unique identifier.', 'wp-graphql-woocommerce' ), - ], - 'regularPrice' => [ - 'type' => 'Float', - 'description' => __( 'Regular price of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'salePrice' => [ - 'type' => 'Float', - 'description' => __( 'Sale price of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'dateOnSaleFrom' => [ - 'type' => 'String', - 'description' => __( 'Start date of sale price.', 'wp-graphql-woocommerce' ), - ], - 'dateOnSaleTo' => [ - 'type' => 'String', - 'description' => __( 'End date of sale price.', 'wp-graphql-woocommerce' ), - ], - 'visible' => [ - 'type' => 'boolean', - 'description' => __( 'Is product variation public?', 'wp-graphql-woocommerce' ), - ], - 'virtual' => [ - 'type' => 'Boolean', - 'description' => __( 'Whether the product variation is virtual.', 'wp-graphql-woocommerce' ), - ], - 'downloadable' => [ - 'type' => 'Boolean', - 'description' => __( 'Whether the product variation is downloadable.', 'wp-graphql-woocommerce' ), - ], - 'downloads' => [ - 'type' => [ 'list_of' => 'ProductDownloadInput' ], - 'description' => __( 'Downloadable files.', 'wp-graphql-woocommerce' ), - ], - 'downloadLimit' => [ - 'type' => 'Int', - 'description' => __( 'Number of times downloadable files can be downloaded.', 'wp-graphql-woocommerce' ), - ], - 'downloadExpiry' => [ - 'type' => 'Int', - 'description' => __( 'Number of days until the download expires.', 'wp-graphql-woocommerce' ), - ], - 'taxStatus' => [ - 'type' => 'TaxStatusEnum', - 'description' => __( 'Tax status of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'taxClass' => [ - 'type' => 'String', - 'description' => __( 'Tax class of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'manageStock' => [ - 'type' => 'String', - 'description' => __( 'Whether to manage stock. Either "yes", "no", or "parent".', 'wp-graphql-woocommerce' ), - ], - 'stockQuantity' => [ - 'type' => 'Int', - 'description' => __( 'Stock quantity.', 'wp-graphql-woocommerce' ), - ], - 'stockStatus' => [ - 'type' => 'StockStatusEnum', - 'description' => __( 'Stock status of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'backorders' => [ - 'type' => 'BackordersEnum', - 'description' => __( 'Backorder status.', 'wp-graphql-woocommerce' ), - ], - 'weight' => [ - 'type' => 'String', - 'description' => __( 'Weight of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'dimensions' => [ - 'type' => 'ProductDimensionsInput', - 'description' => __( 'Dimensions of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'shippingClass' => [ - 'type' => 'String', - 'description' => __( 'Shipping class of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'image' => [ - 'type' => 'ProductImageInput', - 'description' => __( 'Image of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'attributes' => [ - 'type' => [ 'list_of' => 'ProductAttributeInput' ], - 'description' => __( 'Attributes of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'menuOrder' => [ - 'type' => 'Int', - 'description' => __( 'Menu order of the product variation.', 'wp-graphql-woocommerce' ), - ], - 'metaData' => [ - 'type' => [ 'list_of' => 'MetaDataInput' ], - 'description' => __( 'Meta data of the product variation.', 'wp-graphql-woocommerce' ), - ], - ]; - } - - /** + 'productId' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Description of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'sku' => [ + 'type' => 'String', + 'description' => __( 'Unique identifier.', 'wp-graphql-woocommerce' ), + ], + 'regularPrice' => [ + 'type' => 'Float', + 'description' => __( 'Regular price of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'salePrice' => [ + 'type' => 'Float', + 'description' => __( 'Sale price of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleFrom' => [ + 'type' => 'String', + 'description' => __( 'Start date of sale price.', 'wp-graphql-woocommerce' ), + ], + 'dateOnSaleTo' => [ + 'type' => 'String', + 'description' => __( 'End date of sale price.', 'wp-graphql-woocommerce' ), + ], + 'visible' => [ + 'type' => 'boolean', + 'description' => __( 'Is product variation public?', 'wp-graphql-woocommerce' ), + ], + 'virtual' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the product variation is virtual.', 'wp-graphql-woocommerce' ), + ], + 'downloadable' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the product variation is downloadable.', 'wp-graphql-woocommerce' ), + ], + 'downloads' => [ + 'type' => [ 'list_of' => 'ProductDownloadInput' ], + 'description' => __( 'Downloadable files.', 'wp-graphql-woocommerce' ), + ], + 'downloadLimit' => [ + 'type' => 'Int', + 'description' => __( 'Number of times downloadable files can be downloaded.', 'wp-graphql-woocommerce' ), + ], + 'downloadExpiry' => [ + 'type' => 'Int', + 'description' => __( 'Number of days until the download expires.', 'wp-graphql-woocommerce' ), + ], + 'taxStatus' => [ + 'type' => 'TaxStatusEnum', + 'description' => __( 'Tax status of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'taxClass' => [ + 'type' => 'String', + 'description' => __( 'Tax class of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'manageStock' => [ + 'type' => 'String', + 'description' => __( 'Whether to manage stock. Either "yes", "no", or "parent".', 'wp-graphql-woocommerce' ), + ], + 'stockQuantity' => [ + 'type' => 'Int', + 'description' => __( 'Stock quantity.', 'wp-graphql-woocommerce' ), + ], + 'stockStatus' => [ + 'type' => 'StockStatusEnum', + 'description' => __( 'Stock status of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'backorders' => [ + 'type' => 'BackordersEnum', + 'description' => __( 'Backorder status.', 'wp-graphql-woocommerce' ), + ], + 'weight' => [ + 'type' => 'String', + 'description' => __( 'Weight of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'dimensions' => [ + 'type' => 'ProductDimensionsInput', + 'description' => __( 'Dimensions of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'shippingClass' => [ + 'type' => 'String', + 'description' => __( 'Shipping class of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'image' => [ + 'type' => 'ProductImageInput', + 'description' => __( 'Image of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'attributes' => [ + 'type' => [ 'list_of' => 'ProductAttributeInput' ], + 'description' => __( 'Attributes of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'menuOrder' => [ + 'type' => 'Int', + 'description' => __( 'Menu order of the product variation.', 'wp-graphql-woocommerce' ), + ], + 'metaData' => [ + 'type' => [ 'list_of' => 'MetaDataInput' ], + 'description' => __( 'Meta data of the product variation.', 'wp-graphql-woocommerce' ), + ], + ]; + } + + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'variation' => [ + 'variation' => [ 'type' => 'ProductVariation', 'resolve' => static function ( $payload ) { return new Product_Variation( $payload['id'] ); @@ -166,171 +166,202 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * - * @return callable + * @param array $input Mutation input. + * @param \WPGraphQL\AppContext $context AppContext instance. + * @param \GraphQL\Type\Definition\ResolveInfo $info ResolveInfo instance. Can be + * use to get info about the current node in the GraphQL tree. + * + * @throws \GraphQL\Error\UserError Invalid ID provided | Lack of capabilities. + * + * @return array */ public static function mutate_and_get_payload( $input, AppContext $context, ResolveInfo $info ) { - if ( ! empty( $input['id'] ) ) { - $variation = \wc_get_product( $input['id'] ); - } else { - $variation = new \WC_Product_Variation(); - } - - if ( 0 === $variation->get_parent_id() ) { - $variation->set_parent_id( $input['productId'] ); - } - - if ( isset( $input['visible'] ) ) { - $variation->set_status( false === $input['visible'] ? 'private' : 'publish' ); - } - - if ( ! empty( $input['sku'] ) ) { - $variation->set_sku( wc_clean( $input['sku'] ) ); - } - - if ( ! empty( $input['image'] ) ) { - $image = $input['image']; - $image['position'] = 0; - - $variation = Product_Mutation::set_product_image( $variation, [ $image ] ); - } else { - $variation->set_image_id( '' ); - } - - if ( isset( $input['virtual'] ) ) { - $variation->set_virtual( $input['virtual'] ); - } - - if ( isset( $input['downloadable'] ) ) { - $variation->set_downloadable( $input['downloadable'] ); - } - - if ( $variation->get_downloadable() ) { - if ( ! empty( $request['downloads'] ) ) { - $variation = Product_Mutation::save_downloadable_files( $variation, $input['downloads'] ); - } - - if ( ! empty( $input['downloadLimit'] ) ) { - $variation->set_download_limit( $input['downloadLimit'] ); - } - - if ( ! empty( $input['downloadExpiry'] ) ) { - $variation->set_download_expiry( $input['downloadExpiry'] ); - } - } - - $variation = Product_Mutation::save_product_shipping_data( $variation, $input ); - - if ( isset( $input['manageStock'] ) ) { - if ( 'parent' === $input['manageStock'] ) { - $variation->set_manage_stock( false ); - } else { - $variation->set_manage_stock( wc_string_to_bool( $input['manageStock'] ) ); - } - } - - if ( isset( $input['stockStatus'] ) ) { - $variation->set_stock_status( $input['stockStatus'] ); - } - - if ( isset( $input['backorders'] ) ) { - $variation->set_backorders( $input['backorders'] ); - } - - if ( $variation->get_manage_stock() ) { - if ( isset( $input['stockQuantity'] ) ) { - $variation->set_stock_quantity( $input['stockQuantity'] ); - } - } else { - $variation->set_backorders( 'no' ); - $variation->set_stock_quantity( '' ); - } - - if ( isset( $input['regularPrice'] ) ) { - $variation->set_regular_price( $input['regularPrice'] ); - } - - if ( isset( $input['salePrice'] ) ) { - $variation->set_sale_price( $input['salePrice'] ); - } - - if ( isset( $input['dateOnSaleFrom'] ) ) { - $variation->set_date_on_sale_from( $input['dateOnSaleFrom'] ); - } - - if ( isset( $input['dateOnSaleTo'] ) ) { - $variation->set_date_on_sale_to( $input['dateOnSaleTo'] ); - } - - if ( isset( $input['taxClass'] ) ) { - $variation->set_tax_class( $input['taxClass'] ); - } - - if ( isset( $input['description'] ) ) { - $variation->set_description( $input['description'] ); - } - - if ( ! empty( $input['attributes'] ) ) { - $attributes = []; - $parent = wc_get_product( $variation->get_parent_id() ); - $parent_attributes = $parent->get_attributes(); - - foreach ( $input['attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['attributeName'] ) ) { - $raw_attribute_name = sanitize_title( $attribute['attributeName'] ); - } - - if ( ! $attribute_id && ! $raw_attribute_name ) { - continue; - } - - $attribute_name = sanitize_title( $raw_attribute_name ); - - if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { - continue; - } - - $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); - $attribute_value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : ''; - - if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { - // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + if ( ! empty( $input['id'] ) ) { + /** + * @var \WC_Product_Variation $variation + */ + $variation = \wc_get_product( $input['id'] ); + } else { + $variation = new \WC_Product_Variation(); + } + + if ( 0 === $variation->get_parent_id() ) { + $variation->set_parent_id( $input['productId'] ); + } + + if ( isset( $input['visible'] ) ) { + $variation->set_status( false === $input['visible'] ? 'private' : 'publish' ); + } + + if ( ! empty( $input['sku'] ) ) { + /** + * @var string $sku + */ + $sku = wc_clean( $input['sku'] ); + $variation->set_sku( $sku ); + } + + if ( ! empty( $input['image'] ) ) { + $image = $input['image']; + $image['position'] = 0; + + $variation = Product_Mutation::set_product_images( $variation, [ $image ] ); + } else { + $variation->set_image_id( '' ); + } + + if ( isset( $input['virtual'] ) ) { + $variation->set_virtual( $input['virtual'] ); + } + + if ( isset( $input['downloadable'] ) ) { + $variation->set_downloadable( $input['downloadable'] ); + } + + if ( $variation->get_downloadable() ) { + if ( ! empty( $input['downloads'] ) ) { + $variation = Product_Mutation::save_downloadable_files( $variation, $input['downloads'] ); + } + + if ( ! empty( $input['downloadLimit'] ) ) { + $variation->set_download_limit( $input['downloadLimit'] ); + } + + if ( ! empty( $input['downloadExpiry'] ) ) { + $variation->set_download_expiry( $input['downloadExpiry'] ); + } + } + + $variation = Product_Mutation::save_product_shipping_data( $variation, $input ); + + if ( isset( $input['manageStock'] ) ) { + if ( 'parent' === $input['manageStock'] ) { + $variation->set_manage_stock( false ); + } else { + $variation->set_manage_stock( wc_string_to_bool( $input['manageStock'] ) ); + } + } + + if ( isset( $input['stockStatus'] ) ) { + $variation->set_stock_status( $input['stockStatus'] ); + } + + if ( isset( $input['backorders'] ) ) { + $variation->set_backorders( $input['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $input['stockQuantity'] ) ) { + $variation->set_stock_quantity( $input['stockQuantity'] ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( null ); + } + + if ( isset( $input['regularPrice'] ) ) { + $variation->set_regular_price( $input['regularPrice'] ); + } + + if ( isset( $input['salePrice'] ) ) { + $variation->set_sale_price( $input['salePrice'] ); + } + + if ( isset( $input['dateOnSaleFrom'] ) ) { + $variation->set_date_on_sale_from( $input['dateOnSaleFrom'] ); + } + + if ( isset( $input['dateOnSaleTo'] ) ) { + $variation->set_date_on_sale_to( $input['dateOnSaleTo'] ); + } + + if ( isset( $input['taxClass'] ) ) { + $variation->set_tax_class( $input['taxClass'] ); + } + + if ( isset( $input['description'] ) ) { + $variation->set_description( $input['description'] ); + } + + if ( ! empty( $input['attributes'] ) ) { + $attributes = []; + $parent = wc_get_product( $variation->get_parent_id() ); + if ( ! $parent ) { + throw new UserError( __( 'Parent ID invalid', 'wp-graphql-woocommerce' ) ); + } + $parent_attributes = $parent->get_attributes(); + + foreach ( $input['attributes'] as $attribute ) { + /** + * Attribute ID. + * + * @var int $attribute_id + */ + $attribute_id = 0; + /** + * Attribute name. + * + * @var string $attribute_name + */ + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + $raw_attribute_name = null; + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['attributeName'] ) ) { + $raw_attribute_name = sanitize_title( $attribute['attributeName'] ); + } + + if ( ! $raw_attribute_name ) { + continue; + } + + $attribute_name = sanitize_title( $raw_attribute_name ); + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + /** + * @var string $attribute_value + */ + $attribute_value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. $term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine - if ( $term && ! is_wp_error( $term ) ) { - $attribute_value = $term->slug; - } else { - $attribute_value = sanitize_title( $attribute_value ); - } - } + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } - $attributes[ $attribute_key ] = $attribute_value; - } + $attributes[ $attribute_key ] = $attribute_value; + } - $variation->set_attributes( $attributes ); - } + $variation->set_attributes( $attributes ); + } - if ( ! empty( $input['menuOrder'] ) ) { - $variation->set_menu_order( $input['menuOrder'] ); - } + if ( ! empty( $input['menuOrder'] ) ) { + $variation->set_menu_order( $input['menuOrder'] ); + } - if ( ! empty( $input['metaData'] ) ) { - foreach ( $input['metaData'] as $meta ) { - $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); - } - } + if ( ! empty( $input['metaData'] ) ) { + foreach ( $input['metaData'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } - $variation_id = $variation->save(); + $variation_id = $variation->save(); - return [ 'id' => $variation_id ]; - } + return [ 'id' => $variation_id ]; + } } diff --git a/includes/mutation/class-product-variation-delete.php b/includes/mutation/class-product-variation-delete.php index 75a6980b..d70d7f3b 100644 --- a/includes/mutation/class-product-variation-delete.php +++ b/includes/mutation/class-product-variation-delete.php @@ -13,7 +13,6 @@ use GraphQL\Error\UserError; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; use WPGraphQL\WooCommerce\Model\Product_Variation; /** @@ -36,32 +35,32 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array */ public static function get_input_fields() { return [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), - ], + 'id' => [ + 'type' => [ 'non_null' => 'ID' ], + 'description' => __( 'Unique identifier for the product.', 'wp-graphql-woocommerce' ), + ], 'force' => [ 'type' => 'Boolean', 'description' => __( 'Whether to bypass trash and force deletion.', 'wp-graphql-woocommerce' ), ], - ]; - } + ]; + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'variation' => [ + 'variation' => [ 'type' => 'ProductVariation', 'resolve' => static function ( $payload ) { return $payload['variation']; @@ -70,19 +69,19 @@ public static function get_output_fields() { ]; } - /** + /** * Defines the mutation data modification closure. * * @return callable */ public static function mutate_and_get_payload() { return static function ( $input, AppContext $context, ResolveInfo $info ) { - $variation_id = $input['id']; + $variation_id = $input['id']; $force = isset( $input['force'] ) ? $input['force'] : false; $object = new Product_Variation( $variation_id ); $result = false; - if ( ! $object || 0 === $object->get_id() ) { + if ( 0 === $object->ID ) { throw new UserError( __( 'Invalid product variation ID.', 'wp-graphql-woocommerce' ) ); } @@ -93,16 +92,21 @@ public static function mutate_and_get_payload() { * * Return false to disable trash support for the object. * - * @param boolean $supports_trash Whether the object type support trashing. - * @param \WC_Product $object The object being considered for trashing support. + * @param boolean $supports_trash Whether the object type support trashing. + * @param \WPGraphQL\WooCommerce\Model\Product_Variation $object The object being considered for trashing support. */ - $supports_trash = apply_filters( "graphql_woocommerce_product_variation_object_trashable", $supports_trash, $object ); + $supports_trash = apply_filters( 'graphql_woocommerce_product_variation_object_trashable', $supports_trash, $object ); - if ( ! wc_rest_check_post_permissions( 'product_variation', 'delete', $object->get_id() ) ) { + if ( ! wc_rest_check_post_permissions( 'product_variation', 'delete', $object->ID ) ) { throw new UserError( __( 'Sorry, you are not allowed to delete product variations', 'wp-graphql-woocommerce' ) ); } - $variation_to_be_deleted = \wc_get_product( $object->get_id() ); + /** + * Get the variation to be deleted. + * + * @var \WC_Product_Variation $variation_to_be_deleted + */ + $variation_to_be_deleted = \wc_get_product( $object->ID ); if ( $force ) { $variation_to_be_deleted->delete( true ); @@ -113,13 +117,18 @@ public static function mutate_and_get_payload() { throw new UserError( __( 'This product variation does not support trashing.', 'wp-graphql-woocommerce' ) ); } - if ( is_callable( array( $variation_to_be_deleted, 'get_status' ) ) ) { + if ( is_callable( [ $variation_to_be_deleted, 'get_status' ] ) ) { if ( 'trash' === $variation_to_be_deleted->get_status() ) { throw new UserError( __( 'Product variation is already in the trash.', 'wp-graphql-woocommerce' ) ); } $variation_to_be_deleted->delete(); - $result = 'trash' === $variation_to_be_deleted->get_status(); + + /** + * @var string $status + */ + $status = $variation_to_be_deleted->get_status(); + $result = 'trash' === $status; } } @@ -134,12 +143,12 @@ public static function mutate_and_get_payload() { /** * Fires after a single object is deleted or trashed via the REST API. * - * @param Product_Variation $object The deleted or trashed object. + * @param \WPGraphQL\WooCommerce\Model\Product_Variation $object The deleted or trashed object. * @param array $input The mutation input. */ - do_action( "graphql_woocommerce_delete_product_variation_object", $object, $input ); + do_action( 'graphql_woocommerce_delete_product_variation_object', $object, $input ); - return [ 'variation' => $object ]; - }; - } + return [ 'variation' => $object ]; + }; + } } diff --git a/includes/mutation/class-product-variation-update.php b/includes/mutation/class-product-variation-update.php index 686c8bee..46c8433b 100644 --- a/includes/mutation/class-product-variation-update.php +++ b/includes/mutation/class-product-variation-update.php @@ -10,10 +10,6 @@ namespace WPGraphQL\WooCommerce\Mutation; -use GraphQL\Error\UserError; -use GraphQL\Type\Definition\ResolveInfo; -use WPGraphQL\AppContext; -use WPGraphQL\WooCommerce\Data\Mutation\Product_Mutation; use WPGraphQL\WooCommerce\Model\Product_Variation; /** @@ -36,7 +32,7 @@ public static function register_mutation() { ); } - /** + /** * Defines the mutation input field configuration * * @return array @@ -51,16 +47,16 @@ public static function get_input_fields() { ], Product_Variation_Create::get_input_fields() ); - } + } - /** + /** * Defines the mutation output field configuration * * @return array */ public static function get_output_fields() { return [ - 'variation' => [ + 'variation' => [ 'type' => 'ProductVariation', 'resolve' => static function ( $payload ) { return new Product_Variation( $payload['id'] ); diff --git a/includes/type/input/class-product-attributes-input.php b/includes/type/input/class-product-attributes-input.php index 3be70ac3..695f4070 100644 --- a/includes/type/input/class-product-attributes-input.php +++ b/includes/type/input/class-product-attributes-input.php @@ -23,30 +23,30 @@ public static function register() { [ 'description' => __( 'Product attribute properties', 'wp-graphql-woocommerce' ), 'fields' => [ - 'id' => [ - 'type' => 'Int', - 'description' => __( 'Attribute ID', 'wp-graphql-woocommerce' ), - ], - 'name' => [ - 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'Attribute name', 'wp-graphql-woocommerce' ), - ], - 'position' => [ - 'type' => 'Int', - 'description' => __( 'Attribute position', 'wp-graphql-woocommerce' ), - ], - 'visible' => [ - 'type' => 'Boolean', - 'description' => __( 'Define if the attribute is visible on the "Additional information" tab in the product\'s page. Default is false.', 'wp-graphql-woocommerce' ), - ], - 'variation' => [ - 'type' => 'Boolean', - 'description' => __( 'Define if the attribute can be used as variation. Default is false.', 'wp-graphql-woocommerce' ), - ], - 'options' => [ - 'type' => [ 'list_of' => 'String' ], - 'description' => __( 'List of available term names for the attribute', 'wp-graphql-woocommerce' ), - ], + 'id' => [ + 'type' => 'Int', + 'description' => __( 'Attribute ID', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'Attribute name', 'wp-graphql-woocommerce' ), + ], + 'position' => [ + 'type' => 'Int', + 'description' => __( 'Attribute position', 'wp-graphql-woocommerce' ), + ], + 'visible' => [ + 'type' => 'Boolean', + 'description' => __( 'Define if the attribute is visible on the "Additional information" tab in the product\'s page. Default is false.', 'wp-graphql-woocommerce' ), + ], + 'variation' => [ + 'type' => 'Boolean', + 'description' => __( 'Define if the attribute can be used as variation. Default is false.', 'wp-graphql-woocommerce' ), + ], + 'options' => [ + 'type' => [ 'list_of' => 'String' ], + 'description' => __( 'List of available term names for the attribute', 'wp-graphql-woocommerce' ), + ], ], ] ); diff --git a/includes/type/input/class-product-dimensions-input.php b/includes/type/input/class-product-dimensions-input.php index 7ee40795..5608f34d 100644 --- a/includes/type/input/class-product-dimensions-input.php +++ b/includes/type/input/class-product-dimensions-input.php @@ -24,17 +24,17 @@ public static function register() { 'description' => __( 'Product dimensions', 'wp-graphql-woocommerce' ), 'fields' => [ 'length' => [ - 'type' => 'String', - 'description' => __( 'Length of the product', 'wp-graphql-woocommerce' ), - ], - 'width' => [ - 'type' => 'String', - 'description' => __( 'Width of the product', 'wp-graphql-woocommerce' ), - ], - 'height' => [ - 'type' => 'String', - 'description' => __( 'Height of the product', 'wp-graphql-woocommerce' ), - ], + 'type' => 'String', + 'description' => __( 'Length of the product', 'wp-graphql-woocommerce' ), + ], + 'width' => [ + 'type' => 'String', + 'description' => __( 'Width of the product', 'wp-graphql-woocommerce' ), + ], + 'height' => [ + 'type' => 'String', + 'description' => __( 'Height of the product', 'wp-graphql-woocommerce' ), + ], ], ] ); diff --git a/includes/type/input/class-product-download-input.php b/includes/type/input/class-product-download-input.php index 38d5a3c7..957b3130 100644 --- a/includes/type/input/class-product-download-input.php +++ b/includes/type/input/class-product-download-input.php @@ -23,18 +23,18 @@ public static function register() { [ 'description' => __( 'Product download', 'wp-graphql-woocommerce' ), 'fields' => [ - 'id' => [ - 'type' => 'Int', - 'description' => __( 'File ID', 'wp-graphql-woocommerce' ), - ], - 'name' => [ - 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'File name', 'wp-graphql-woocommerce' ), - ], - 'file' => [ - 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'File URL', 'wp-graphql-woocommerce' ), - ], + 'id' => [ + 'type' => 'Int', + 'description' => __( 'File ID', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'File name', 'wp-graphql-woocommerce' ), + ], + 'file' => [ + 'type' => [ 'non_null' => 'String' ], + 'description' => __( 'File URL', 'wp-graphql-woocommerce' ), + ], ], ] ); diff --git a/includes/type/input/class-product-image-input.php b/includes/type/input/class-product-image-input.php index 27490291..40dd9b62 100644 --- a/includes/type/input/class-product-image-input.php +++ b/includes/type/input/class-product-image-input.php @@ -23,22 +23,22 @@ public static function register() { [ 'description' => __( 'Product image', 'wp-graphql-woocommerce' ), 'fields' => [ - 'id' => [ - 'type' => 'Int', - 'description' => __( 'Image ID', 'wp-graphql-woocommerce' ), - ], - 'src' => [ - 'type' => 'String', - 'description' => __( 'Image URL', 'wp-graphql-woocommerce' ), - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'Image name', 'wp-graphql-woocommerce' ), - ], - 'altText' => [ - 'type' => 'String', - 'description' => __( 'Image alternative text', 'wp-graphql-woocommerce' ), - ], + 'id' => [ + 'type' => 'Int', + 'description' => __( 'Image ID', 'wp-graphql-woocommerce' ), + ], + 'src' => [ + 'type' => 'String', + 'description' => __( 'Image URL', 'wp-graphql-woocommerce' ), + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Image name', 'wp-graphql-woocommerce' ), + ], + 'altText' => [ + 'type' => 'String', + 'description' => __( 'Image alternative text', 'wp-graphql-woocommerce' ), + ], ], ] ); diff --git a/includes/type/object/class-product-attribute-object-type.php b/includes/type/object/class-product-attribute-object-type.php index 51029aca..03d603f8 100644 --- a/includes/type/object/class-product-attribute-object-type.php +++ b/includes/type/object/class-product-attribute-object-type.php @@ -21,55 +21,55 @@ class Product_Attribute_Object_Type { */ public static function register() { register_graphql_object_type( - 'ProductAttributeObject', - [ - 'description' => __( 'Product attribute object.', 'wp-graphql-woocommerce' ), - 'eagerlyLoadType' => true, - 'fields' => [ - 'id' => [ - 'type' => 'ID', - 'description' => __( 'Unique identifier for the product attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->attribute_id ) ? $source->attribute_id : null; - }, - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->attribute_name ) ? (string) $source->attribute_name : null; - }, - ], - 'label' => [ - 'type' => 'String', - 'description' => __( 'Label of the attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->attribute_label ) ? (string) $source->attribute_label : null; - }, - ], - 'type' => [ - 'type' => 'String', - 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->attribute_type ) ? (string) $source->attribute_type : null; - }, - ], - 'orderBy' => [ - 'type' => 'String', - 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->attribute_orderby ) ? (string) $source->attribute_orderby : null; - }, - ], - 'hasArchives' => [ - 'type' => 'Boolean', - 'description' => __( 'Whether or not the attribute has archives.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return isset( $source->attribute_public ) ? $source->attribute_public : false; - }, - ], - ], - ] - ); - } -} \ No newline at end of file + 'ProductAttributeObject', + [ + 'description' => __( 'Product attribute object.', 'wp-graphql-woocommerce' ), + 'eagerlyLoadType' => true, + 'fields' => [ + 'id' => [ + 'type' => 'ID', + 'description' => __( 'Unique identifier for the product attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->attribute_id ) ? $source->attribute_id : null; + }, + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->attribute_name ) ? (string) $source->attribute_name : null; + }, + ], + 'label' => [ + 'type' => 'String', + 'description' => __( 'Label of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->attribute_label ) ? (string) $source->attribute_label : null; + }, + ], + 'type' => [ + 'type' => 'String', + 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->attribute_type ) ? (string) $source->attribute_type : null; + }, + ], + 'orderBy' => [ + 'type' => 'String', + 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->attribute_orderby ) ? (string) $source->attribute_orderby : null; + }, + ], + 'hasArchives' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether or not the attribute has archives.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return isset( $source->attribute_public ) ? $source->attribute_public : false; + }, + ], + ], + ] + ); + } +} diff --git a/includes/type/object/class-product-attribute-term-object-type.php b/includes/type/object/class-product-attribute-term-object-type.php index 6ab2f0d1..6f14b624 100644 --- a/includes/type/object/class-product-attribute-term-object-type.php +++ b/includes/type/object/class-product-attribute-term-object-type.php @@ -21,55 +21,55 @@ class Product_Attribute_Term_Object_Type { */ public static function register() { register_graphql_object_type( - 'ProductAttributeTermObject', - [ - 'description' => __( 'Product attribute object.', 'wp-graphql-woocommerce' ), - 'eagerlyLoadType' => true, - 'fields' => [ - 'id' => [ - 'type' => 'Integer', - 'description' => __( 'Unique identifier for the product attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->id ) ? $source->id : null; - }, - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->name ) ? $source->name : null; - }, - ], - 'slug' => [ - 'type' => 'String', - 'description' => __( 'Label of the attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->slug ) ? $source->slug : null; - }, - ], - 'description' => [ - 'type' => 'String', - 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return ! empty( $source->description ) ? $source->description : null; - }, - ], - 'menuOrder' => [ - 'type' => 'Integer', - 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return isset( $source->menu_order ) ? $source->menu_order : 0; - }, - ], - 'count' => [ - 'type' => 'Integer', - 'description' => __( 'Whether or not the attribute has archives.', 'wp-graphql-woocommerce' ), - 'resolve' => static function( $source ) { - return isset( $source->count ) ? $source->count : 0; - }, - ], - ], - ] - ); - } -} \ No newline at end of file + 'ProductAttributeTermObject', + [ + 'description' => __( 'Product attribute object.', 'wp-graphql-woocommerce' ), + 'eagerlyLoadType' => true, + 'fields' => [ + 'id' => [ + 'type' => 'Integer', + 'description' => __( 'Unique identifier for the product attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->id ) ? $source->id : null; + }, + ], + 'name' => [ + 'type' => 'String', + 'description' => __( 'Name of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->name ) ? $source->name : null; + }, + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'Label of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->slug ) ? $source->slug : null; + }, + ], + 'description' => [ + 'type' => 'String', + 'description' => __( 'Type of the attribute.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return ! empty( $source->description ) ? $source->description : null; + }, + ], + 'menuOrder' => [ + 'type' => 'Integer', + 'description' => __( 'Order by which the attribute should be sorted.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return isset( $source->menu_order ) ? $source->menu_order : 0; + }, + ], + 'count' => [ + 'type' => 'Integer', + 'description' => __( 'Whether or not the attribute has archives.', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + return isset( $source->count ) ? $source->count : 0; + }, + ], + ], + ] + ); + } +} diff --git a/phpstan/constants.php b/phpstan/constants.php index 6833eeb7..1e2b7acc 100644 --- a/phpstan/constants.php +++ b/phpstan/constants.php @@ -11,3 +11,4 @@ define( 'WPGRAPHQL_WOOCOMMERCE_PLUGIN_DIR', '' ); define( 'WPGRAPHQL_WOOCOMMERCE_PLUGIN_URL', '' ); define( 'WC_SESSION_CACHE_GROUP', '' ); +define( 'WC_DELIMITER', '|' ); diff --git a/tests/_data/test-product.jpg b/tests/_data/test-product.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6c7b4d54fb8bb192a1a1b9304a010f6e9cebe7db GIT binary patch literal 23041 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 $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 ); diff --git a/tests/wpunit/ProductAttributeMutationsTest.php b/tests/wpunit/ProductAttributeMutationsTest.php index 8ccb29b0..b95497d7 100644 --- a/tests/wpunit/ProductAttributeMutationsTest.php +++ b/tests/wpunit/ProductAttributeMutationsTest.php @@ -25,6 +25,19 @@ public function testCreateProductAttribute() { 'hasArchives' => false, ], ]; + + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( @@ -66,6 +79,8 @@ public function testUpdateProductAttribute() { 'hasArchives' => false, ], ]; + + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( @@ -105,6 +120,20 @@ public function testUpdateProductAttribute() { 'hasArchives' => true, ], ]; + + // Assert mutation fails as unauthenticated user. + $this->loginAs( 0 ); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( @@ -142,6 +171,8 @@ public function testDeleteProductAttribute() { 'hasArchives' => false, ], ]; + + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $this->assertQuerySuccessful( $response ); @@ -163,6 +194,20 @@ public function testDeleteProductAttribute() { 'id' => $attribute_id, ], ]; + + // Assert mutation fails as unauthenticated user. + $this->loginAs( 0 ); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( diff --git a/tests/wpunit/ProductAttributeTermMutationsTest.php b/tests/wpunit/ProductAttributeTermMutationsTest.php index 2f70a8f3..129911d4 100644 --- a/tests/wpunit/ProductAttributeTermMutationsTest.php +++ b/tests/wpunit/ProductAttributeTermMutationsTest.php @@ -2,7 +2,7 @@ class ProductAttributeTermMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { public function testCreateProductAttributeTerm() { - $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special' ] ); $query = ' mutation ($input: CreateProductAttributeTermInput!) { @@ -29,6 +29,18 @@ public function testCreateProductAttributeTerm() { ], ]; + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( @@ -48,7 +60,7 @@ public function testCreateProductAttributeTerm() { } public function testUpdateProductAttributeTerm() { - $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special', 'hated' ] ); + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special', 'hated' ] ); $hated_term_id = get_term_by( 'slug', 'hated', 'pa_kind' )->term_id; $query = ' @@ -77,6 +89,18 @@ public function testUpdateProductAttributeTerm() { ], ]; + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( @@ -96,7 +120,7 @@ public function testUpdateProductAttributeTerm() { } public function testDeleteProductAttributeTerm() { - $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special', 'hated' ] ); + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special', 'hated' ] ); $hated_term_id = get_term_by( 'slug', 'hated', 'pa_kind' )->term_id; $query = ' @@ -117,6 +141,18 @@ public function testDeleteProductAttributeTerm() { ], ]; + // Assert mutation fails as unauthenticated user. + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + + // Assert mutation fails as authenticated user without proper capabilities. + $this->loginAsCustomer(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertQueryError( $response ); + + // Assert mutation succeeds as authenticated user with proper capabilities. + $this->loginAsShopManager(); $response = $this->graphql( compact( 'query', 'variables' ) ); $expected = [ $this->expectedObject( diff --git a/tests/wpunit/ProductMutationsTest.php b/tests/wpunit/ProductMutationsTest.php index f64895de..2f4cecab 100644 --- a/tests/wpunit/ProductMutationsTest.php +++ b/tests/wpunit/ProductMutationsTest.php @@ -2,6 +2,11 @@ class ProductMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase { public function testCreateProduct() { + // Create timestamps. + $yesterday = strtotime( '-1 day' ); + $tomorrow = strtotime( '+1 day' ); + + // Create query. $query = ' mutation ( $input: CreateProductInput! ) { createProduct(input: $input) { @@ -9,15 +14,28 @@ public function testCreateProduct() { id name type + purchaseNote dateOnSaleFrom dateOnSaleTo + reviewsAllowed ... on ProductWithPricing { price regularPrice salePrice + taxClass + taxStatus } ... on InventoriedProduct { stockQuantity + stockStatus + soldIndividually + } + ... on ProductWithDimensions { + length + width + height + shippingRequired + shippingTaxable } attributes { nodes { @@ -32,17 +50,27 @@ public function testCreateProduct() { } '; + // Create variables. $variables = [ 'input' => [ - 'name' => 'Test Product', - 'type' => 'SIMPLE', - 'regularPrice' => 10, - 'salePrice' => 7, - 'dateOnSaleFrom' => date( 'Y-m-d H:i:s', strtotime( '-1 day' ) ), - 'dateOnSaleTo' => date( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), - 'manageStock' => true, - 'stockStatus' => 'IN_STOCK', - 'stockQuantity' => 3, + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + 'salePrice' => 7, + 'dateOnSaleFrom' => date( 'Y-m-d H:i:s', $yesterday ), + 'dateOnSaleTo' => date( 'Y-m-d H:i:s', $tomorrow ), + 'manageStock' => true, + 'stockQuantity' => 3, + 'soldIndividually' => true, + 'dimensions' => [ + 'length' => '40cm', + 'width' => '5in', + 'height' => '2ft', + ], + 'purchaseNote' => 'Test note', + 'taxStatus' => 'TAXABLE', + 'taxClass' => 'STANDARD', + 'reviewsAllowed' => true, ] ]; @@ -69,7 +97,214 @@ public function testCreateProduct() { $this->expectedField( 'price', "$7.00" ), $this->expectedField( 'regularPrice', "$10.00" ), $this->expectedField( 'salePrice', "$7.00" ), + $this->expectedField( 'dateOnSaleFrom', date( 'Y-m-d\TH:i:sP', $yesterday ) ), + $this->expectedField( 'dateOnSaleTo', date( 'Y-m-d\TH:i:sP', $tomorrow ) ), $this->expectedField( 'stockQuantity', 3 ), + $this->expectedField( 'stockStatus', 'IN_STOCK' ), + $this->expectedField( 'soldIndividually', true ), + $this->expectedField( 'length', '40' ), + $this->expectedField( 'width', '5' ), + $this->expectedField( 'height', '2' ), + $this->expectedField( 'purchaseNote', 'Test note' ), + $this->expectedField( 'taxStatus', 'TAXABLE' ), + $this->expectedField( 'taxClass', 'STANDARD' ), + $this->expectedField( 'shippingRequired', true ), + $this->expectedField( 'shippingTaxable', true ), + $this->expectedField( 'reviewsAllowed', true ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreateProductWithImages() { + // Create images. + $image_ids = array_map( + function( $id ) { + return $this->factory->attachment->create_upload_object( + __DIR__ . '/../_data/test-product.jpg', + $id + ); + }, + $this->factory->attachment->create_many( + 5, + [ + 'post_mime_type' => 'image/gif', + 'post_author' => $this->admin, + ] + ) + ); + + // Create query. + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + image { id } + galleryImages { nodes { id } } + } + } + } + '; + + // Create variables. + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + ] + ]; + $variables['input']['images'] = array_map( + function( $id ) { + return [ 'id' => $id ]; + }, + $image_ids + ); + + // Run mutation. + $this->loginAsShopManager(); + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // Assert response. + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedField( 'image.id', $this->toRelayId( 'post', $image_ids[0] ) ), + $this->expectedObject( + 'galleryImages', + [ + $this->expectedField( 'nodes.#.id', $this->toRelayId( 'post', $image_ids[1] ) ), + $this->expectedField( 'nodes.#.id', $this->toRelayId( 'post', $image_ids[2] ) ), + $this->expectedField( 'nodes.#.id', $this->toRelayId( 'post', $image_ids[3] ) ), + $this->expectedField( 'nodes.#.id', $this->toRelayId( 'post', $image_ids[4] ) ), + ] + ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreateProductWithMetaData() { + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + metaData { + id + key + value + } + } + } + } + '; + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + 'metaData' => [ + [ + 'key' => 'test_key', + 'value' => 'test_value', + ], + [ + 'key' => 'test_key_2', + 'value' => 'test_value_2', + ], + ], + ] + ]; + + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedNode( + 'metaData', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'key', 'test_key' ), + $this->expectedField( 'value', 'test_value' ), + ], + ), + $this->expectedNode( + 'metaData', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'key', 'test_key_2' ), + $this->expectedField( 'value', 'test_value_2' ), + ], + ), + ] + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + public function testCreateProductWithTaxonomies() { + // Create categories and tags. + $clothing_id = $this->factory->product->createProductCategory( 'clothing' ); + $shirts_id = $this->factory->product->createProductCategory( 'shirts', $clothing_id ); + $new_id = $this->factory->product->createProductTag( 'new' ); + + // Create query. + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + productCategories { nodes { name } } + productTags { nodes { name } } + } + } + } + '; + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + 'categories' => [ $shirts_id ], + 'tags' => [ $new_id ], + ] + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + $this->expectedField( 'productCategories.nodes.#.name', 'shirts' ), + $this->expectedField( 'productTags.nodes.#.name', 'new' ), + $this->not()->expectedField( 'productCategories.nodes.#.id', 'clothing' ), ] ), ]; @@ -77,6 +312,65 @@ public function testCreateProduct() { $this->assertQuerySuccessful( $response, $expected ); } + public function testCreateProductWithUpsellAndCrossSell() { + $upsell_ids = $this->factory->product->createManySimple(5); + $cross_sell_ids = $this->factory->product->createManySimple(5); + + $query = ' + mutation ( $input: CreateProductInput! ) { + createProduct(input: $input) { + product { + id + name + type + upsell { nodes { id } } + ... on SimpleProduct { + crossSell { nodes { id } } + } + } + } + } + '; + + $this->loginAsShopManager(); + $variables = [ + 'input' => [ + 'name' => 'Test Product', + 'type' => 'SIMPLE', + 'regularPrice' => 10, + 'upsellIds' => $upsell_ids, + 'crossSellIds' => $cross_sell_ids, + ] + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedObject( + 'createProduct.product', + array_merge( + [ + $this->expectedField( 'id', self::NOT_NULL ), + $this->expectedField( 'name', 'Test Product' ), + $this->expectedField( 'type', 'SIMPLE' ), + ], + array_map( + function( $id ) { + return $this->expectedField( 'upsell.nodes.#.id', $this->toRelayId( 'product', $id ) ); + }, + $upsell_ids + ), + array_map( + function( $id ) { + return $this->expectedField( 'crossSell.nodes.#.id', $this->toRelayId( 'product', $id ) ); + }, + $cross_sell_ids + ) + ) + ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + public function testCreateProductWithAttributes() { $query = ' mutation ( $input: CreateProductInput! ) { @@ -98,7 +392,7 @@ public function testCreateProductWithAttributes() { } '; - $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special' ] ); $this->loginAsShopManager(); $variables = [ @@ -178,7 +472,7 @@ public function testCreatingVariableProductWithCreateProduct() { } '; - $kind_attribute = $this->factory()->product->createAttribute( 'kind', [ 'normal', 'special' ] ); + $kind_attribute = $this->factory->product->createAttribute( 'kind', [ 'normal', 'special' ] ); $this->loginAsShopManager(); $variables = [ @@ -321,8 +615,8 @@ public function testCreatingExternalProductWithCreateProduct() { public function testCreatingGroupProductWithCreateProduct() { $product_ids = [ - $this->factory()->product->createSimple( [ 'name' => 'Product 1' ] ), - $this->factory()->product->createSimple( [ 'name' => 'Product 2' ] ), + $this->factory->product->createSimple( [ 'name' => 'Product 1' ] ), + $this->factory->product->createSimple( [ 'name' => 'Product 2' ] ), ]; $query = ' @@ -371,7 +665,7 @@ public function testCreatingGroupProductWithCreateProduct() { } public function testUpdateProduct() { - $product_id = $this->factory()->product->createSimple( + $product_id = $this->factory->product->createSimple( [ 'name' => 'Test Product', 'regular_price' => 10, @@ -452,7 +746,7 @@ public function testUpdateProduct() { } public function testDeleteProduct() { - $product_id = $this->factory()->product->createSimple([ + $product_id = $this->factory->product->createSimple([ 'name' => 'Test Product', 'regular_price' => 10, 'sale_price' => 7,