diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index f34665f..b37dc5a 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -28,7 +28,30 @@ jobs: find includes -name "*.php" -print0 | xargs -0 -n1 php -l find tests -name "*.php" -print0 | xargs -0 -n1 php -l + phpcs: + name: PHP CodeSniffer + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, xml + tools: composer:v2 + + - name: Install Composer dependencies + run: | + composer config platform.php 8.3.0 + composer install --optimize-autoloader --no-interaction + + - name: Run PHPCS + run: vendor/bin/phpcs + test: + name: PHP Unit runs-on: ubuntu-latest needs: lint steps: @@ -51,7 +74,7 @@ jobs: run: vendor/bin/phpunit --testdox build-release: - needs: test + needs: [test, phpcs] runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index df89653..216bb1a 100755 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ phpunit.xml languages/*.mo .phpunit.result.cache +.phpunit.cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c9e24dd..2a5ae14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **PHPUnit test suite**: 57 unit tests covering all 6 core classes (ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings) - **Brain Monkey + Mockery** for WordPress/WooCommerce function mocking without a full WP installation - **WooCommerce class stubs** in `tests/stubs/` for classes extended by the plugin (WC_Product, WC_Settings_Page, etc.) +- **PHPCS** with WordPress-Extra and PHPCompatibilityWP coding standards (`phpcs.xml.dist`) +- **PHPCS job** in release workflow — coding standards must pass before release is built - **PHP lint job** in release workflow — syntax-checks all PHP files before testing -- **Test job** in release workflow — tests must pass before release package is built (`needs: test`) -- Testing section in README +- **Test job** in release workflow — tests must pass before release package is built +- Testing and linting sections in README and CLAUDE.md ### Changed - **PSR-4 refactored**: Renamed files to PascalCase (Product_Type → ProductType, etc.) and changed namespace from `WC_Composable_Product` to `Magdev\WcComposableProduct` - Updated all cross-references in PHP files, main plugin file, composer.json, CSS/JS doc comments, and translation file source comments -- Release workflow now has three stages: lint → test → build-release +- **PHPUnit upgraded** from 9.6 to 10 (Brain Monkey 2.7 supports both) +- **WPCS formatting** applied to all source files (tabs, Yoda conditions, strict `in_array`, `wp_json_encode`, long array syntax) +- Release workflow now has four stages: lint + phpcs (parallel) → test → build-release +- Composer platform pinned to PHP 8.3 to prevent incompatible dependency locks ## [1.3.0] - 2026-03-01 diff --git a/CLAUDE.md b/CLAUDE.md index 4fc05f8..49a6729 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,8 @@ This project is 100% AI-generated ("vibe-coded") using Claude.AI. - **Styling:** Custom CSS - **Dependencies:** Composer - **i18n:** WordPress i18n (.pot/.po/.mo), text domain: `wc-composable-product` -- **Testing:** PHPUnit 9.6 + Brain Monkey 2.7 + Mockery 1.6 +- **Testing:** PHPUnit 10 + Brain Monkey 2.7 + Mockery 1.6 +- **Linting:** PHPCS 3.13 with WordPress-Extra + PHPCompatibilityWP - **CI/CD:** Gitea Actions (`.gitea/workflows/release.yml`) ## Project Structure @@ -55,6 +56,7 @@ wc-composable-product/ │ └── product-selector.twig # Frontend selection interface ├── vendor/ # Composer dependencies (gitignored, included in releases) ├── composer.json +├── phpcs.xml.dist # PHPCS configuration (WordPress-Extra + PHPCompatibilityWP) ├── phpunit.xml.dist # PHPUnit configuration └── wc-composable-product.php # Main plugin file ``` @@ -140,11 +142,15 @@ Compile .po to .mo: `for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po" WordPress requires compiled .mo files — .po files alone are insufficient. -## Testing +## Testing & Linting Run unit tests: `vendor/bin/phpunit --testdox` -Tests use **Brain Monkey** to mock WordPress/WooCommerce functions without a full WP installation. WooCommerce classes (`WC_Product`, `WC_Settings_Page`, etc.) are provided as minimal stubs in `tests/stubs/` so PHP can parse `extends` declarations. The release workflow runs tests before building — a failing test blocks the release. +Run coding standards check: `vendor/bin/phpcs` + +Auto-fix coding standard violations: `vendor/bin/phpcbf` + +Tests use **Brain Monkey** to mock WordPress/WooCommerce functions without a full WP installation. WooCommerce classes (`WC_Product`, `WC_Settings_Page`, etc.) are provided as minimal stubs in `tests/stubs/` so PHP can parse `extends` declarations. PHPCS uses the **WordPress-Extra** standard plus **PHPCompatibilityWP** for PHP version checks. The release workflow runs lint, phpcs, and tests before building — any failure blocks the release. ## Release Workflow diff --git a/composer.json b/composer.json index 85041e9..66599ed 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,12 @@ }, "require-dev": { "brain/monkey": "^2.7", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "mockery/mockery": "^1.6", - "phpunit/phpunit": "^9.6" + "phpcompatibility/phpcompatibility-wp": "*", + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "^3.7", + "wp-coding-standards/wpcs": "^3.0" }, "autoload-dev": { "psr-4": { @@ -30,6 +34,12 @@ }, "config": { "optimize-autoloader": true, - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "platform": { + "php": "8.3.0" + } } } diff --git a/composer.lock b/composer.lock index 93b0a46..9b9db91 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": "b1604d49fcc5b210c5bcc61f22c43b8e", + "content-hash": "c9478958ffb6c2696b9f2f0dc41ac4c3", "packages": [ { "name": "symfony/deprecation-contracts", @@ -441,35 +441,39 @@ "time": "2026-02-05T09:22:14+00:00" }, { - "name": "doctrine/instantiator", - "version": "2.1.0", + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { - "php": "^8.4" + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { - "doctrine/coding-standard": "^14", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58" + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, - "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -478,36 +482,59 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", "keywords": [ - "constructor", - "instantiate" + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" + "url": "https://github.com/PHPCSStandards", + "type": "github" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/jrfnl", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2026-01-05T06:47:08+00:00" + "time": "2025-11-11T04:32:07+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -880,17 +907,405 @@ "time": "2022-02-21T01:04:05+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-09-19T17:43:28+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.8", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-10-18T00:05:59+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { @@ -898,18 +1313,18 @@ "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^10.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -918,7 +1333,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -947,7 +1362,7 @@ "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.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -955,32 +1370,32 @@ "type": "github" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1007,7 +1422,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { @@ -1015,28 +1431,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" @@ -1044,7 +1460,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1070,7 +1486,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" }, "funding": [ { @@ -1078,32 +1494,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2023-02-03T06:56:09+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1129,7 +1545,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { @@ -1137,32 +1554,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1188,7 +1605,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" }, "funding": [ { @@ -1196,24 +1613,23 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2023-02-03T06:57:52+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.34", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b36f02317466907a230d3aa1d34467041271ef4a" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", - "reference": "b36f02317466907a230d3aa1d34467041271ef4a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1223,27 +1639,26 @@ "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.10", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.8", - "sebastian/global-state": "^5.0.8", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "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" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -1251,7 +1666,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "10.5-dev" } }, "autoload": { @@ -1283,7 +1698,7 @@ "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.34" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -1307,32 +1722,32 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:45:00+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -1355,7 +1770,8 @@ "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" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -1363,32 +1779,32 @@ "type": "github" } ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -1411,7 +1827,7 @@ "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" + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" }, "funding": [ { @@ -1419,32 +1835,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2023-02-03T06:58:43+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1466,7 +1882,7 @@ "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" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" }, "funding": [ { @@ -1474,34 +1890,36 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2023-02-03T06:59:15+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.10", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", - "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -1540,7 +1958,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -1560,33 +1979,33 @@ "type": "tidelift" } ], - "time": "2026-01-24T09:22:56+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -1609,7 +2028,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" }, "funding": [ { @@ -1617,33 +2037,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1675,7 +2095,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -1683,27 +2104,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" @@ -1711,7 +2132,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -1730,7 +2151,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -1738,7 +2159,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -1746,34 +2168,34 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -1815,7 +2237,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { @@ -1835,38 +2258,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.8", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1885,59 +2305,48 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://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.8" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", - "type": "tidelift" } ], - "time": "2025-08-10T07:10:35+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -1960,7 +2369,8 @@ "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" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -1968,34 +2378,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2017,7 +2427,7 @@ "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" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { @@ -2025,32 +2435,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2023-02-03T07:08:32+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2072,7 +2482,7 @@ "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" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { @@ -2080,32 +2490,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.6", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2135,7 +2545,8 @@ "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.6" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" }, "funding": [ { @@ -2155,86 +2566,32 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:57: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" + "time": "2025-08-10T07:50:56+00:00" }, { "name": "sebastian/type", - "version": "3.2.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2257,7 +2614,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, "funding": [ { @@ -2265,29 +2622,29 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2023-02-03T07:10:45+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2310,7 +2667,7 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { @@ -2318,7 +2675,86 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" }, { "name": "theseer/tokenizer", @@ -2369,6 +2805,72 @@ } ], "time": "2025-11-17T20:03:58+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" } ], "aliases": [], @@ -2380,5 +2882,8 @@ "php": ">=8.3" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.3.0" + }, "plugin-api-version": "2.6.0" } diff --git a/includes/Admin/ProductData.php b/includes/Admin/ProductData.php index f9801bd..481ca66 100644 --- a/includes/Admin/ProductData.php +++ b/includes/Admin/ProductData.php @@ -7,211 +7,239 @@ namespace Magdev\WcComposableProduct\Admin; -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; /** * Product Data Tab Class */ class ProductData { - /** - * Constructor - */ - public function __construct() { - add_filter('woocommerce_product_data_tabs', [$this, 'add_product_data_tab']); - add_action('woocommerce_product_data_panels', [$this, 'add_product_data_panel']); - add_action('woocommerce_process_product_meta_composable', [$this, 'save_product_data']); - add_action('woocommerce_product_options_general_product_data', [$this, 'add_general_fields']); - } + /** + * Constructor + */ + public function __construct() { + add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tab' ) ); + add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panel' ) ); + add_action( 'woocommerce_process_product_meta_composable', array( $this, 'save_product_data' ) ); + add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_general_fields' ) ); + } - /** - * Add composable products tab - * - * @param array $tabs Product data tabs - * @return array - */ - public function add_product_data_tab($tabs) { - $tabs['composable'] = [ - 'label' => __('Composable Options', 'wc-composable-product'), - 'target' => 'composable_product_data', - 'class' => ['show_if_composable'], - 'priority' => 21, - ]; - return $tabs; - } + /** + * Add composable products tab + * + * @param array $tabs Product data tabs + * @return array + */ + public function add_product_data_tab( $tabs ) { + $tabs['composable'] = array( + 'label' => __( 'Composable Options', 'wc-composable-product' ), + 'target' => 'composable_product_data', + 'class' => array( 'show_if_composable' ), + 'priority' => 21, + ); + return $tabs; + } - /** - * Add fields to general tab - */ - public function add_general_fields() { - global $product_object; + /** + * Add fields to general tab + */ + public function add_general_fields() { + global $product_object; - if ($product_object && $product_object->get_type() === 'composable') { - echo '
'; + if ( $product_object && $product_object->get_type() === 'composable' ) { + echo '
'; - woocommerce_wp_text_input([ - 'id' => '_composable_selection_limit', - 'label' => __('Selection Limit', 'wc-composable-product'), - 'description' => __('Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product'), - 'desc_tip' => true, - 'type' => 'number', - 'custom_attributes' => [ - 'min' => '1', - 'step' => '1', - ], - ]); + woocommerce_wp_text_input( + array( + 'id' => '_composable_selection_limit', + 'label' => __( 'Selection Limit', 'wc-composable-product' ), + 'description' => __( 'Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product' ), + 'desc_tip' => true, + 'type' => 'number', + 'custom_attributes' => array( + 'min' => '1', + 'step' => '1', + ), + ) + ); - woocommerce_wp_select([ - 'id' => '_composable_pricing_mode', - 'label' => __('Pricing Mode', 'wc-composable-product'), - 'description' => __('How to calculate the price.', 'wc-composable-product'), - 'desc_tip' => true, - 'options' => [ - '' => __('Use global default', 'wc-composable-product'), - 'sum' => __('Sum of selected products', 'wc-composable-product'), - 'fixed' => __('Fixed price', 'wc-composable-product'), - ], - ]); + woocommerce_wp_select( + array( + 'id' => '_composable_pricing_mode', + 'label' => __( 'Pricing Mode', 'wc-composable-product' ), + 'description' => __( 'How to calculate the price.', 'wc-composable-product' ), + 'desc_tip' => true, + 'options' => array( + '' => __( 'Use global default', 'wc-composable-product' ), + 'sum' => __( 'Sum of selected products', 'wc-composable-product' ), + 'fixed' => __( 'Fixed price', 'wc-composable-product' ), + ), + ) + ); - woocommerce_wp_text_input([ - 'id' => '_regular_price', - 'label' => __('Fixed Price', 'wc-composable-product') . ' (' . get_woocommerce_currency_symbol() . ')', - 'description' => __('Enter the fixed price for this composable product.', 'wc-composable-product'), - 'desc_tip' => true, - 'type' => 'text', - 'data_type' => 'price', - 'wrapper_class' => 'composable_fixed_price_field', - ]); + woocommerce_wp_text_input( + array( + 'id' => '_regular_price', + 'label' => __( 'Fixed Price', 'wc-composable-product' ) . ' (' . get_woocommerce_currency_symbol() . ')', + 'description' => __( 'Enter the fixed price for this composable product.', 'wc-composable-product' ), + 'desc_tip' => true, + 'type' => 'text', + 'data_type' => 'price', + 'wrapper_class' => 'composable_fixed_price_field', + ) + ); - echo '
'; - } - } + echo '
'; + } + } - /** - * Add product data panel - */ - public function add_product_data_panel() { - global $post; - ?> - + + id = 'composable_products'; - $this->label = __('Composable Products', 'wc-composable-product'); + /** + * Constructor + */ + public function __construct() { + $this->id = 'composable_products'; + $this->label = __( 'Composable Products', 'wc-composable-product' ); - parent::__construct(); - } + parent::__construct(); + } - /** - * Get settings array - * - * @return array - */ - public function get_settings() { - $settings = [ - [ - 'title' => __('Composable Products Settings', 'wc-composable-product'), - 'type' => 'title', - 'desc' => __('Configure default settings for composable products.', 'wc-composable-product'), - 'id' => 'wc_composable_settings', - ], - [ - 'title' => __('Default Selection Limit', 'wc-composable-product'), - 'desc' => __('Default number of items customers can select.', 'wc-composable-product'), - 'id' => 'wc_composable_default_limit', - 'type' => 'number', - 'default' => '5', - 'custom_attributes' => [ - 'min' => '1', - 'step' => '1', - ], - 'desc_tip' => true, - ], - [ - 'title' => __('Default Pricing Mode', 'wc-composable-product'), - 'desc' => __('How to calculate the price of composable products.', 'wc-composable-product'), - 'id' => 'wc_composable_default_pricing', - 'type' => 'select', - 'default' => 'sum', - 'options' => [ - 'sum' => __('Sum of selected products', 'wc-composable-product'), - 'fixed' => __('Fixed price', 'wc-composable-product'), - ], - 'desc_tip' => true, - ], - [ - 'title' => __('Include Non-Public Products', 'wc-composable-product'), - 'desc' => __('Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually.', 'wc-composable-product'), - 'id' => 'wc_composable_include_unpublished', - 'type' => 'checkbox', - 'default' => 'no', - ], - [ - 'title' => __('Show Product Images', 'wc-composable-product'), - 'desc' => __('Display product images in the selection interface.', 'wc-composable-product'), - 'id' => 'wc_composable_show_images', - 'type' => 'checkbox', - 'default' => 'yes', - ], - [ - 'title' => __('Show Product Prices', 'wc-composable-product'), - 'desc' => __('Display individual product prices in the selection interface.', 'wc-composable-product'), - 'id' => 'wc_composable_show_prices', - 'type' => 'checkbox', - 'default' => 'yes', - ], - [ - 'title' => __('Show Total Price', 'wc-composable-product'), - 'desc' => __('Display the total price as customers make selections.', 'wc-composable-product'), - 'id' => 'wc_composable_show_total', - 'type' => 'checkbox', - 'default' => 'yes', - ], - [ - 'type' => 'sectionend', - 'id' => 'wc_composable_settings', - ], - ]; + /** + * Get settings array + * + * @return array + */ + public function get_settings() { + $settings = array( + array( + 'title' => __( 'Composable Products Settings', 'wc-composable-product' ), + 'type' => 'title', + 'desc' => __( 'Configure default settings for composable products.', 'wc-composable-product' ), + 'id' => 'wc_composable_settings', + ), + array( + 'title' => __( 'Default Selection Limit', 'wc-composable-product' ), + 'desc' => __( 'Default number of items customers can select.', 'wc-composable-product' ), + 'id' => 'wc_composable_default_limit', + 'type' => 'number', + 'default' => '5', + 'custom_attributes' => array( + 'min' => '1', + 'step' => '1', + ), + 'desc_tip' => true, + ), + array( + 'title' => __( 'Default Pricing Mode', 'wc-composable-product' ), + 'desc' => __( 'How to calculate the price of composable products.', 'wc-composable-product' ), + 'id' => 'wc_composable_default_pricing', + 'type' => 'select', + 'default' => 'sum', + 'options' => array( + 'sum' => __( 'Sum of selected products', 'wc-composable-product' ), + 'fixed' => __( 'Fixed price', 'wc-composable-product' ), + ), + 'desc_tip' => true, + ), + array( + 'title' => __( 'Include Non-Public Products', 'wc-composable-product' ), + 'desc' => __( 'Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually.', 'wc-composable-product' ), + 'id' => 'wc_composable_include_unpublished', + 'type' => 'checkbox', + 'default' => 'no', + ), + array( + 'title' => __( 'Show Product Images', 'wc-composable-product' ), + 'desc' => __( 'Display product images in the selection interface.', 'wc-composable-product' ), + 'id' => 'wc_composable_show_images', + 'type' => 'checkbox', + 'default' => 'yes', + ), + array( + 'title' => __( 'Show Product Prices', 'wc-composable-product' ), + 'desc' => __( 'Display individual product prices in the selection interface.', 'wc-composable-product' ), + 'id' => 'wc_composable_show_prices', + 'type' => 'checkbox', + 'default' => 'yes', + ), + array( + 'title' => __( 'Show Total Price', 'wc-composable-product' ), + 'desc' => __( 'Display the total price as customers make selections.', 'wc-composable-product' ), + 'id' => 'wc_composable_show_total', + 'type' => 'checkbox', + 'default' => 'yes', + ), + array( + 'type' => 'sectionend', + 'id' => 'wc_composable_settings', + ), + ); - return apply_filters('wc_composable_settings', $settings); - } + return apply_filters( 'wc_composable_settings', $settings ); + } - /** - * Output the settings - */ - public function output() { - $settings = $this->get_settings(); - \WC_Admin_Settings::output_fields($settings); - } + /** + * Output the settings + */ + public function output() { + $settings = $this->get_settings(); + \WC_Admin_Settings::output_fields( $settings ); + } - /** - * Save settings - */ - public function save() { - $settings = $this->get_settings(); - \WC_Admin_Settings::save_fields($settings); - } + /** + * Save settings + */ + public function save() { + $settings = $this->get_settings(); + \WC_Admin_Settings::save_fields( $settings ); + } } diff --git a/includes/CartHandler.php b/includes/CartHandler.php index 72017db..6e68721 100644 --- a/includes/CartHandler.php +++ b/includes/CartHandler.php @@ -7,7 +7,7 @@ namespace Magdev\WcComposableProduct; -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; /** * Cart Handler Class @@ -15,207 +15,214 @@ defined('ABSPATH') || exit; * Handles adding composable products to cart and calculating prices */ class CartHandler { - /** - * Stock manager instance - * - * @var StockManager - */ - private $stock_manager; + /** + * Stock manager instance + * + * @var StockManager + */ + private $stock_manager; - /** - * Constructor - */ - public function __construct() { - $this->stock_manager = new StockManager(); + /** + * Constructor + */ + public function __construct() { + $this->stock_manager = new StockManager(); - add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3); - add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2); - add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 2); - add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2); - add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']); - add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25); - add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3); - add_filter('woocommerce_is_purchasable', [$this, 'hide_default_add_to_cart'], 10, 2); - } + add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'validate_add_to_cart' ), 10, 3 ); + add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 2 ); + add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'get_cart_item_from_session' ), 10, 2 ); + add_filter( 'woocommerce_get_item_data', array( $this, 'display_cart_item_data' ), 10, 2 ); + add_action( 'woocommerce_before_calculate_totals', array( $this, 'calculate_cart_item_price' ) ); + add_action( 'woocommerce_single_product_summary', array( $this, 'render_product_selector' ), 25 ); + add_action( 'woocommerce_checkout_create_order_line_item', array( $this->stock_manager, 'store_selected_products_in_order' ), 10, 3 ); + add_filter( 'woocommerce_is_purchasable', array( $this, 'hide_default_add_to_cart' ), 10, 2 ); + } - /** - * Hide default WooCommerce add to cart button for composable products - * - * @param bool $is_purchasable Is purchasable status - * @param \WC_Product $product Product object - * @return bool - */ - public function hide_default_add_to_cart($is_purchasable, $product) { - if ($product && $product->get_type() === 'composable') { - return false; - } - return $is_purchasable; - } + /** + * Hide default WooCommerce add to cart button for composable products + * + * @param bool $is_purchasable Is purchasable status + * @param \WC_Product $product Product object + * @return bool + */ + public function hide_default_add_to_cart( $is_purchasable, $product ) { + if ( $product && $product->get_type() === 'composable' ) { + return false; + } + return $is_purchasable; + } - /** - * Render product selector on product page - */ - public function render_product_selector() { - global $product; + /** + * Render product selector on product page + */ + public function render_product_selector() { + global $product; - if ($product && $product->get_type() === 'composable') { - ProductSelector::render($product); - } - } + if ( $product && $product->get_type() === 'composable' ) { + ProductSelector::render( $product ); + } + } - /** - * Validate add to cart - * - * @param bool $passed Validation status - * @param int $product_id Product ID - * @param int $quantity Quantity - * @return bool - */ - public function validate_add_to_cart($passed, $product_id, $quantity) { - $product = wc_get_product($product_id); + /** + * Validate add to cart + * + * @param bool $passed Validation status + * @param int $product_id Product ID + * @param int $quantity Quantity + * @return bool + */ + public function validate_add_to_cart( $passed, $product_id, $quantity ) { + $product = wc_get_product( $product_id ); - if (!$product || $product->get_type() !== 'composable') { - return $passed; - } + if ( ! $product || $product->get_type() !== 'composable' ) { + return $passed; + } - // Check if selected products are provided - if (!isset($_POST['composable_products']) || empty($_POST['composable_products'])) { - wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error'); - return false; - } + // Check if selected products are provided. + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler. + if ( ! isset( $_POST['composable_products'] ) || empty( $_POST['composable_products'] ) ) { + wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' ); + return false; + } - $selected_products = array_map('absint', $_POST['composable_products']); - $selection_limit = $product->get_selection_limit(); + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler. + $selected_products = array_map( 'absint', $_POST['composable_products'] ); + $selection_limit = $product->get_selection_limit(); - // Validate selection limit - if (count($selected_products) > $selection_limit) { - /* translators: %d: selection limit */ - wc_add_notice(sprintf(__('You can select a maximum of %d products.', 'wc-composable-product'), $selection_limit), 'error'); - return false; - } + // Validate selection limit + if ( count( $selected_products ) > $selection_limit ) { + /* translators: %d: selection limit */ + wc_add_notice( sprintf( __( 'You can select a maximum of %d products.', 'wc-composable-product' ), $selection_limit ), 'error' ); + return false; + } - if (count($selected_products) === 0) { - wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error'); - return false; - } + if ( count( $selected_products ) === 0 ) { + wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' ); + return false; + } - // Validate that selected products are valid - $available_products = $product->get_available_products(); - $available_ids = array_map(function($p) { - return $p->get_id(); - }, $available_products); + // Validate that selected products are valid + $available_products = $product->get_available_products(); + $available_ids = array_map( + function ( $p ) { + return $p->get_id(); + }, + $available_products + ); - foreach ($selected_products as $selected_id) { - if (!in_array($selected_id, $available_ids)) { - wc_add_notice(__('One or more selected products are not available.', 'wc-composable-product'), 'error'); - return false; - } - } + foreach ( $selected_products as $selected_id ) { + if ( ! in_array( $selected_id, $available_ids, true ) ) { + wc_add_notice( __( 'One or more selected products are not available.', 'wc-composable-product' ), 'error' ); + return false; + } + } - // Validate stock availability - $stock_validation = $this->stock_manager->validate_stock_availability($selected_products, $quantity); - if ($stock_validation !== true) { - wc_add_notice($stock_validation, 'error'); - return false; - } + // Validate stock availability + $stock_validation = $this->stock_manager->validate_stock_availability( $selected_products, $quantity ); + if ( true !== $stock_validation ) { + wc_add_notice( $stock_validation, 'error' ); + return false; + } - return $passed; - } + return $passed; + } - /** - * Add cart item data - * - * @param array $cart_item_data Cart item data - * @param int $product_id Product ID - * @return array - */ - public function add_cart_item_data($cart_item_data, $product_id) { - $product = wc_get_product($product_id); + /** + * Add cart item data + * + * @param array $cart_item_data Cart item data + * @param int $product_id Product ID + * @return array + */ + public function add_cart_item_data( $cart_item_data, $product_id ) { + $product = wc_get_product( $product_id ); - if (!$product || $product->get_type() !== 'composable') { - return $cart_item_data; - } + if ( ! $product || $product->get_type() !== 'composable' ) { + return $cart_item_data; + } - if (isset($_POST['composable_products']) && !empty($_POST['composable_products'])) { - $selected_products = array_map('absint', $_POST['composable_products']); - $cart_item_data['composable_products'] = $selected_products; + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler. + if ( isset( $_POST['composable_products'] ) && ! empty( $_POST['composable_products'] ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler. + $selected_products = array_map( 'absint', $_POST['composable_products'] ); + $cart_item_data['composable_products'] = $selected_products; - // Make cart item unique - $cart_item_data['unique_key'] = md5(json_encode($selected_products) . time()); - } + // Make cart item unique. + $cart_item_data['unique_key'] = md5( wp_json_encode( $selected_products ) . time() ); + } - return $cart_item_data; - } + return $cart_item_data; + } - /** - * Get cart item from session - * - * @param array $cart_item Cart item - * @param array $values Values from session - * @return array - */ - public function get_cart_item_from_session($cart_item, $values) { - if (isset($values['composable_products'])) { - $cart_item['composable_products'] = $values['composable_products']; - } + /** + * Get cart item from session + * + * @param array $cart_item Cart item + * @param array $values Values from session + * @return array + */ + public function get_cart_item_from_session( $cart_item, $values ) { + if ( isset( $values['composable_products'] ) ) { + $cart_item['composable_products'] = $values['composable_products']; + } - return $cart_item; - } + return $cart_item; + } - /** - * Display cart item data - * - * @param array $item_data Item data - * @param array $cart_item Cart item - * @return array - */ - public function display_cart_item_data($item_data, $cart_item) { - if (isset($cart_item['composable_products']) && !empty($cart_item['composable_products'])) { - $product_names = []; - foreach ($cart_item['composable_products'] as $product_id) { - $product = wc_get_product($product_id); - if ($product) { - $product_names[] = $product->get_name(); - } - } + /** + * Display cart item data + * + * @param array $item_data Item data + * @param array $cart_item Cart item + * @return array + */ + public function display_cart_item_data( $item_data, $cart_item ) { + if ( isset( $cart_item['composable_products'] ) && ! empty( $cart_item['composable_products'] ) ) { + $product_names = array(); + foreach ( $cart_item['composable_products'] as $product_id ) { + $product = wc_get_product( $product_id ); + if ( $product ) { + $product_names[] = $product->get_name(); + } + } - if (!empty($product_names)) { - $item_data[] = [ - 'key' => __('Selected Products', 'wc-composable-product'), - 'value' => implode(', ', $product_names), - ]; - } - } + if ( ! empty( $product_names ) ) { + $item_data[] = array( + 'key' => __( 'Selected Products', 'wc-composable-product' ), + 'value' => implode( ', ', $product_names ), + ); + } + } - return $item_data; - } + return $item_data; + } - /** - * Calculate cart item price - * - * @param \WC_Cart $cart Cart object - */ - public function calculate_cart_item_price($cart) { - if (is_admin() && !defined('DOING_AJAX')) { - return; - } + /** + * Calculate cart item price + * + * @param \WC_Cart $cart Cart object + */ + public function calculate_cart_item_price( $cart ) { + if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { + return; + } - // Use static flag to prevent multiple executions within the same request - static $already_calculated = false; - if ($already_calculated) { - return; - } + // Use static flag to prevent multiple executions within the same request + static $already_calculated = false; + if ( $already_calculated ) { + return; + } - foreach ($cart->get_cart() as $cart_item_key => $cart_item) { - if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') { - if (isset($cart_item['composable_products'])) { - $product = $cart_item['data']; - $price = $product->calculate_composed_price($cart_item['composable_products']); - $cart_item['data']->set_price($price); - } - } - } + foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) { + if ( isset( $cart_item['data'] ) && $cart_item['data']->get_type() === 'composable' ) { + if ( isset( $cart_item['composable_products'] ) ) { + $product = $cart_item['data']; + $price = $product->calculate_composed_price( $cart_item['composable_products'] ); + $cart_item['data']->set_price( $price ); + } + } + } - $already_calculated = true; - } + $already_calculated = true; + } } diff --git a/includes/Plugin.php b/includes/Plugin.php index ae036c2..d46040b 100644 --- a/includes/Plugin.php +++ b/includes/Plugin.php @@ -7,227 +7,240 @@ namespace Magdev\WcComposableProduct; -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; /** * Main plugin class - Singleton pattern */ class Plugin { - /** - * The single instance of the class - * - * @var Plugin - */ - protected static $instance = null; + /** + * The single instance of the class + * + * @var Plugin + */ + protected static $instance = null; - /** - * Twig environment - * - * @var \Twig\Environment - */ - private $twig = null; + /** + * Twig environment + * + * @var \Twig\Environment + */ + private $twig = null; - /** - * Main Plugin Instance - * - * Ensures only one instance is loaded or can be loaded. - * - * @return Plugin - */ - public static function instance() { - if (is_null(self::$instance)) { - self::$instance = new self(); - } - return self::$instance; - } + /** + * Main Plugin Instance + * + * Ensures only one instance is loaded or can be loaded. + * + * @return Plugin + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + } + return self::$instance; + } - /** - * Constructor - */ - private function __construct() { - $this->init_hooks(); - $this->init_twig(); - $this->includes(); - } + /** + * Constructor + */ + private function __construct() { + $this->init_hooks(); + $this->init_twig(); + $this->includes(); + } - /** - * Hook into WordPress and WooCommerce - */ - private function init_hooks() { - // Register product type - add_filter('product_type_selector', [$this, 'add_product_type']); - add_filter('woocommerce_product_class', [$this, 'product_class'], 10, 2); + /** + * Hook into WordPress and WooCommerce + */ + private function init_hooks() { + // Register product type + add_filter( 'product_type_selector', array( $this, 'add_product_type' ) ); + add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 ); - // Enqueue scripts and styles - add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts']); - add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']); + // Enqueue scripts and styles + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); - // Admin settings - add_filter('woocommerce_get_settings_pages', [$this, 'add_settings_page']); - } + // Admin settings + add_filter( 'woocommerce_get_settings_pages', array( $this, 'add_settings_page' ) ); + } - /** - * Initialize Twig template engine - */ - private function init_twig() { - $loader = new \Twig\Loader\FilesystemLoader(WC_COMPOSABLE_PRODUCT_PATH . 'templates'); - $this->twig = new \Twig\Environment($loader, [ - 'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache', - 'auto_reload' => true, - 'debug' => defined('WP_DEBUG') && WP_DEBUG, - ]); + /** + * Initialize Twig template engine + */ + private function init_twig() { + $loader = new \Twig\Loader\FilesystemLoader( WC_COMPOSABLE_PRODUCT_PATH . 'templates' ); + $this->twig = new \Twig\Environment( + $loader, + array( + 'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache', + 'auto_reload' => true, + 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + ) + ); - // Add WordPress functions to Twig - $this->twig->addFunction(new \Twig\TwigFunction('__', function($text) { - return __($text, 'wc-composable-product'); - })); - $this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html')); - $this->twig->addFunction(new \Twig\TwigFunction('esc_attr', 'esc_attr')); - $this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url')); - $this->twig->addFunction(new \Twig\TwigFunction('wc_price', 'wc_price')); + // Add WordPress functions to Twig + $this->twig->addFunction( + new \Twig\TwigFunction( + '__', + function ( $text ) { + // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- dynamic Twig template strings. + return __( $text, 'wc-composable-product' ); + } + ) + ); + $this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) ); + $this->twig->addFunction( new \Twig\TwigFunction( 'esc_attr', 'esc_attr' ) ); + $this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) ); + $this->twig->addFunction( new \Twig\TwigFunction( 'wc_price', 'wc_price' ) ); - // Add WordPress escaping functions as Twig filters - $this->twig->addFilter(new \Twig\TwigFilter('esc_html', 'esc_html')); - $this->twig->addFilter(new \Twig\TwigFilter('esc_attr', 'esc_attr')); - $this->twig->addFilter(new \Twig\TwigFilter('esc_url', 'esc_url')); - } + // Add WordPress escaping functions as Twig filters + $this->twig->addFilter( new \Twig\TwigFilter( 'esc_html', 'esc_html' ) ); + $this->twig->addFilter( new \Twig\TwigFilter( 'esc_attr', 'esc_attr' ) ); + $this->twig->addFilter( new \Twig\TwigFilter( 'esc_url', 'esc_url' ) ); + } - /** - * Include required files - */ - private function includes() { - // Note: Settings.php is NOT included here because it extends WC_Settings_Page - // which isn't loaded until later. It's included in add_settings_page() instead. - require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/ProductData.php'; - require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductType.php'; - require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/StockManager.php'; - require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/CartHandler.php'; - require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductSelector.php'; + /** + * Include required files + */ + private function includes() { + // Note: Settings.php is NOT included here because it extends WC_Settings_Page + // which isn't loaded until later. It's included in add_settings_page() instead. + require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/ProductData.php'; + require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductType.php'; + require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/StockManager.php'; + require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/CartHandler.php'; + require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductSelector.php'; - // Initialize components - new Admin\ProductData(); - new CartHandler(); - } + // Initialize components + new Admin\ProductData(); + new CartHandler(); + } - /** - * Add composable product type to selector - * - * @param array $types Product types - * @return array - */ - public function add_product_type($types) { - $types['composable'] = __('Composable product', 'wc-composable-product'); - return $types; - } + /** + * Add composable product type to selector + * + * @param array $types Product types + * @return array + */ + public function add_product_type( $types ) { + $types['composable'] = __( 'Composable product', 'wc-composable-product' ); + return $types; + } - /** - * Use custom product class for composable products - * - * @param string $classname Product class name - * @param string $product_type Product type - * @return string - */ - public function product_class($classname, $product_type) { - if ($product_type === 'composable') { - $classname = 'Magdev\WcComposableProduct\ProductType'; - } - return $classname; - } + /** + * Use custom product class for composable products + * + * @param string $classname Product class name + * @param string $product_type Product type + * @return string + */ + public function product_class( $classname, $product_type ) { + if ( 'composable' === $product_type ) { + $classname = 'Magdev\WcComposableProduct\ProductType'; + } + return $classname; + } - /** - * Enqueue frontend scripts and styles - */ - public function enqueue_frontend_scripts() { - if (is_product()) { - wp_enqueue_style( - 'wc-composable-product', - WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css', - [], - WC_COMPOSABLE_PRODUCT_VERSION - ); + /** + * Enqueue frontend scripts and styles + */ + public function enqueue_frontend_scripts() { + if ( is_product() ) { + wp_enqueue_style( + 'wc-composable-product', + WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css', + array(), + WC_COMPOSABLE_PRODUCT_VERSION + ); - wp_enqueue_script( - 'wc-composable-product', - WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js', - ['jquery'], - WC_COMPOSABLE_PRODUCT_VERSION, - true - ); + wp_enqueue_script( + 'wc-composable-product', + WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js', + array( 'jquery' ), + WC_COMPOSABLE_PRODUCT_VERSION, + true + ); - wp_localize_script('wc-composable-product', 'wcComposableProduct', [ - 'ajax_url' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('wc_composable_product_nonce'), - 'i18n' => [ - 'select_items' => __('Please select items', 'wc-composable-product'), - 'max_items' => __('Maximum items selected', 'wc-composable-product'), - 'min_items' => __('Please select at least one item', 'wc-composable-product'), - ], - 'price_format' => [ - 'currency_symbol' => get_woocommerce_currency_symbol(), - 'decimal_separator' => wc_get_price_decimal_separator(), - 'thousand_separator' => wc_get_price_thousand_separator(), - 'decimals' => wc_get_price_decimals(), - 'price_format' => get_woocommerce_price_format(), - ], - ]); - } - } + wp_localize_script( + 'wc-composable-product', + 'wcComposableProduct', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'wc_composable_product_nonce' ), + 'i18n' => array( + 'select_items' => __( 'Please select items', 'wc-composable-product' ), + 'max_items' => __( 'Maximum items selected', 'wc-composable-product' ), + 'min_items' => __( 'Please select at least one item', 'wc-composable-product' ), + ), + 'price_format' => array( + 'currency_symbol' => get_woocommerce_currency_symbol(), + 'decimal_separator' => wc_get_price_decimal_separator(), + 'thousand_separator' => wc_get_price_thousand_separator(), + 'decimals' => wc_get_price_decimals(), + 'price_format' => get_woocommerce_price_format(), + ), + ) + ); + } + } - /** - * Enqueue admin scripts and styles - */ - public function enqueue_admin_scripts($hook) { - if ('post.php' === $hook || 'post-new.php' === $hook) { - global $post_type; - if ('product' === $post_type) { - wp_enqueue_style( - 'wc-composable-product-admin', - WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css', - [], - WC_COMPOSABLE_PRODUCT_VERSION - ); + /** + * Enqueue admin scripts and styles + */ + public function enqueue_admin_scripts( $hook ) { + if ( 'post.php' === $hook || 'post-new.php' === $hook ) { + global $post_type; + if ( 'product' === $post_type ) { + wp_enqueue_style( + 'wc-composable-product-admin', + WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css', + array(), + WC_COMPOSABLE_PRODUCT_VERSION + ); - wp_enqueue_script( - 'wc-composable-product-admin', - WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js', - ['jquery', 'wc-admin-product-meta-boxes'], - WC_COMPOSABLE_PRODUCT_VERSION, - true - ); - } - } - } + wp_enqueue_script( + 'wc-composable-product-admin', + WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js', + array( 'jquery', 'wc-admin-product-meta-boxes' ), + WC_COMPOSABLE_PRODUCT_VERSION, + true + ); + } + } + } - /** - * Add settings page to WooCommerce - * - * @param array $settings WooCommerce settings pages - * @return array - */ - public function add_settings_page($settings) { - // Include Settings.php here, when WC_Settings_Page is guaranteed to be loaded - require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php'; - $settings[] = new Admin\Settings(); - return $settings; - } + /** + * Add settings page to WooCommerce + * + * @param array $settings WooCommerce settings pages + * @return array + */ + public function add_settings_page( $settings ) { + // Include Settings.php here, when WC_Settings_Page is guaranteed to be loaded + require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php'; + $settings[] = new Admin\Settings(); + return $settings; + } - /** - * Get Twig environment - * - * @return \Twig\Environment - */ - public function get_twig() { - return $this->twig; - } + /** + * Get Twig environment + * + * @return \Twig\Environment + */ + public function get_twig() { + return $this->twig; + } - /** - * Render a Twig template - * - * @param string $template Template name - * @param array $context Template variables - * @return string - */ - public function render_template($template, $context = []) { - return $this->twig->render($template, $context); - } + /** + * Render a Twig template + * + * @param string $template Template name + * @param array $context Template variables + * @return string + */ + public function render_template( $template, $context = array() ) { + return $this->twig->render( $template, $context ); + } } diff --git a/includes/ProductSelector.php b/includes/ProductSelector.php index b65ea13..9c3902c 100644 --- a/includes/ProductSelector.php +++ b/includes/ProductSelector.php @@ -7,7 +7,7 @@ namespace Magdev\WcComposableProduct; -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; /** * Product Selector Class @@ -15,63 +15,64 @@ defined('ABSPATH') || exit; * Handles rendering the product selection interface */ class ProductSelector { - /** - * Render product selector - * - * @param ProductType $product Composable product - */ - public static function render($product) { - if (!$product || $product->get_type() !== 'composable') { - return; - } + /** + * Render product selector + * + * @param ProductType $product Composable product + */ + public static function render( $product ) { + if ( ! $product || $product->get_type() !== 'composable' ) { + return; + } - $available_products = $product->get_available_products(); - $selection_limit = $product->get_selection_limit(); - $pricing_mode = $product->get_pricing_mode(); + $available_products = $product->get_available_products(); + $selection_limit = $product->get_selection_limit(); + $pricing_mode = $product->get_pricing_mode(); - $show_images = get_option('wc_composable_show_images', 'yes') === 'yes'; - $show_prices = get_option('wc_composable_show_prices', 'yes') === 'yes'; - $show_total = get_option('wc_composable_show_total', 'yes') === 'yes'; + $show_images = get_option( 'wc_composable_show_images', 'yes' ) === 'yes'; + $show_prices = get_option( 'wc_composable_show_prices', 'yes' ) === 'yes'; + $show_total = get_option( 'wc_composable_show_total', 'yes' ) === 'yes'; - // Get stock manager for stock information - $stock_manager = new StockManager(); + // Get stock manager for stock information + $stock_manager = new StockManager(); - // Prepare product data for template - $products_data = []; - foreach ($available_products as $available_product) { - $stock_info = $stock_manager->get_product_stock_info($available_product->get_id()); + // Prepare product data for template + $products_data = array(); + foreach ( $available_products as $available_product ) { + $stock_info = $stock_manager->get_product_stock_info( $available_product->get_id() ); - $products_data[] = [ - 'id' => $available_product->get_id(), - 'name' => $available_product->get_name(), - 'price' => $available_product->get_price(), - 'price_html' => $available_product->get_price_html(), - 'image_url' => wp_get_attachment_image_url($available_product->get_image_id(), 'thumbnail'), - 'permalink' => $available_product->get_permalink(), - 'stock_status' => $stock_info['stock_status'], - 'in_stock' => $stock_info['in_stock'], - 'stock_quantity' => $stock_info['stock_quantity'], - 'managing_stock' => $stock_info['managing_stock'], - 'backorders_allowed' => $stock_info['backorders_allowed'], - ]; - } + $products_data[] = array( + 'id' => $available_product->get_id(), + 'name' => $available_product->get_name(), + 'price' => $available_product->get_price(), + 'price_html' => $available_product->get_price_html(), + 'image_url' => wp_get_attachment_image_url( $available_product->get_image_id(), 'thumbnail' ), + 'permalink' => $available_product->get_permalink(), + 'stock_status' => $stock_info['stock_status'], + 'in_stock' => $stock_info['in_stock'], + 'stock_quantity' => $stock_info['stock_quantity'], + 'managing_stock' => $stock_info['managing_stock'], + 'backorders_allowed' => $stock_info['backorders_allowed'], + ); + } - $context = [ - 'product_id' => $product->get_id(), - 'products' => $products_data, - 'selection_limit' => $selection_limit, - 'pricing_mode' => $pricing_mode, - 'show_images' => $show_images, - 'show_prices' => $show_prices, - 'show_total' => $show_total, - 'fixed_price' => $product->get_price(), - 'fixed_price_html' => wc_price($product->get_price()), - 'zero_price_html' => wc_price(0), - 'currency_symbol' => get_woocommerce_currency_symbol(), - ]; + $context = array( + 'product_id' => $product->get_id(), + 'products' => $products_data, + 'selection_limit' => $selection_limit, + 'pricing_mode' => $pricing_mode, + 'show_images' => $show_images, + 'show_prices' => $show_prices, + 'show_total' => $show_total, + 'fixed_price' => $product->get_price(), + 'fixed_price_html' => wc_price( $product->get_price() ), + 'zero_price_html' => wc_price( 0 ), + 'currency_symbol' => get_woocommerce_currency_symbol(), + ); - // Render template - $plugin = Plugin::instance(); - echo $plugin->render_template('product-selector.twig', $context); - } + // Render template — Twig handles escaping via registered esc_html/esc_attr/esc_url functions. + $plugin = Plugin::instance(); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped by Twig template. + echo $plugin->render_template( 'product-selector.twig', $context ); + } } diff --git a/includes/ProductType.php b/includes/ProductType.php index 4b4386a..b311302 100644 --- a/includes/ProductType.php +++ b/includes/ProductType.php @@ -7,243 +7,248 @@ namespace Magdev\WcComposableProduct; -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; /** * Composable Product Type Class */ class ProductType extends \WC_Product { - /** - * Product type - * - * @var string - */ - protected $product_type = 'composable'; + /** + * Product type + * + * @var string + */ + protected $product_type = 'composable'; - /** - * Constructor - * - * @param mixed $product Product ID or object - */ - public function __construct($product = 0) { - $this->supports[] = 'ajax_add_to_cart'; - parent::__construct($product); - } + /** + * Constructor + * + * @param mixed $product Product ID or object + */ + public function __construct( $product = 0 ) { + $this->supports[] = 'ajax_add_to_cart'; + parent::__construct( $product ); + } - /** - * Get product type - * - * @return string - */ - public function get_type() { - return 'composable'; - } + /** + * Get product type + * + * @return string + */ + public function get_type() { + return 'composable'; + } - /** - * Get selection limit - * - * @return int - */ - public function get_selection_limit() { - $limit = $this->get_meta('_composable_selection_limit', true); - if (empty($limit)) { - $limit = get_option('wc_composable_default_limit', 5); - } - return absint($limit); - } + /** + * Get selection limit + * + * @return int + */ + public function get_selection_limit() { + $limit = $this->get_meta( '_composable_selection_limit', true ); + if ( empty( $limit ) ) { + $limit = get_option( 'wc_composable_default_limit', 5 ); + } + return absint( $limit ); + } - /** - * Get pricing mode - * - * @return string 'fixed' or 'sum' - */ - public function get_pricing_mode() { - $mode = $this->get_meta('_composable_pricing_mode', true); - if (empty($mode)) { - $mode = get_option('wc_composable_default_pricing', 'sum'); - } - return $mode; - } + /** + * Get pricing mode + * + * @return string 'fixed' or 'sum' + */ + public function get_pricing_mode() { + $mode = $this->get_meta( '_composable_pricing_mode', true ); + if ( empty( $mode ) ) { + $mode = get_option( 'wc_composable_default_pricing', 'sum' ); + } + return $mode; + } - /** - * Get product selection criteria - * - * @return array - */ - public function get_selection_criteria() { - return [ - 'type' => $this->get_meta('_composable_criteria_type', true) ?: 'category', - 'categories' => $this->get_meta('_composable_categories', true) ?: [], - 'tags' => $this->get_meta('_composable_tags', true) ?: [], - 'skus' => $this->get_meta('_composable_skus', true) ?: '', - ]; - } + /** + * Get product selection criteria + * + * @return array + */ + public function get_selection_criteria() { + $type = $this->get_meta( '_composable_criteria_type', true ); + $categories = $this->get_meta( '_composable_categories', true ); + $tags = $this->get_meta( '_composable_tags', true ); + $skus = $this->get_meta( '_composable_skus', true ); - /** - * Check if product is purchasable - * - * @return bool - */ - public function is_purchasable() { - return true; - } + return array( + 'type' => $type ? $type : 'category', + 'categories' => $categories ? $categories : array(), + 'tags' => $tags ? $tags : array(), + 'skus' => $skus ? $skus : '', + ); + } - /** - * Check if product is sold individually - * - * @return bool - */ - public function is_sold_individually() { - return true; - } + /** + * Check if product is purchasable + * + * @return bool + */ + public function is_purchasable() { + return true; + } - /** - * Check if non-public products should be included - * - * @return bool - */ - public function should_include_unpublished() { - $per_product = $this->get_meta('_composable_include_unpublished', true); - if ($per_product === 'yes') { - return true; - } - if ($per_product === 'no') { - return false; - } - return get_option('wc_composable_include_unpublished', 'no') === 'yes'; - } + /** + * Check if product is sold individually + * + * @return bool + */ + public function is_sold_individually() { + return true; + } - /** - * Get available products based on criteria - * - * @return array Array of WC_Product objects - */ - public function get_available_products() { - $criteria = $this->get_selection_criteria(); - $include_unpublished = $this->should_include_unpublished(); - $args = [ - 'post_type' => 'product', - 'posts_per_page' => -1, - 'post_status' => $include_unpublished ? ['publish', 'draft', 'private'] : 'publish', - 'orderby' => 'title', - 'order' => 'ASC', - ]; + /** + * Check if non-public products should be included + * + * @return bool + */ + public function should_include_unpublished() { + $per_product = $this->get_meta( '_composable_include_unpublished', true ); + if ( 'yes' === $per_product ) { + return true; + } + if ( 'no' === $per_product ) { + return false; + } + return 'yes' === get_option( 'wc_composable_include_unpublished', 'no' ); + } - // Exclude composable products using the product_type taxonomy - // (WooCommerce stores product types as taxonomy terms, NOT as postmeta) - $args['tax_query'] = [ - 'relation' => 'AND', - [ - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => ['composable'], - 'operator' => 'NOT IN', - ], - ]; + /** + * Get available products based on criteria + * + * @return array Array of WC_Product objects + */ + public function get_available_products() { + $criteria = $this->get_selection_criteria(); + $include_unpublished = $this->should_include_unpublished(); + $args = array( + 'post_type' => 'product', + 'posts_per_page' => -1, + 'post_status' => $include_unpublished ? array( 'publish', 'draft', 'private' ) : 'publish', + 'orderby' => 'title', + 'order' => 'ASC', + ); - switch ($criteria['type']) { - case 'category': - if (!empty($criteria['categories'])) { - $args['tax_query'][] = [ - 'taxonomy' => 'product_cat', - 'field' => 'term_id', - 'terms' => $criteria['categories'], - 'operator' => 'IN', - ]; - } - break; + // Exclude composable products using the product_type taxonomy + // (WooCommerce stores product types as taxonomy terms, NOT as postmeta) + $args['tax_query'] = array( + 'relation' => 'AND', + array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => array( 'composable' ), + 'operator' => 'NOT IN', + ), + ); - case 'tag': - if (!empty($criteria['tags'])) { - $args['tax_query'][] = [ - 'taxonomy' => 'product_tag', - 'field' => 'term_id', - 'terms' => $criteria['tags'], - 'operator' => 'IN', - ]; - } - break; + switch ( $criteria['type'] ) { + case 'category': + if ( ! empty( $criteria['categories'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'field' => 'term_id', + 'terms' => $criteria['categories'], + 'operator' => 'IN', + ); + } + break; - case 'sku': - if (!empty($criteria['skus'])) { - $skus = array_map('trim', explode(',', $criteria['skus'])); - $args['meta_query'] = [ - [ - 'key' => '_sku', - 'value' => $skus, - 'compare' => 'IN', - ], - ]; - } - break; - } + case 'tag': + if ( ! empty( $criteria['tags'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'field' => 'term_id', + 'terms' => $criteria['tags'], + 'operator' => 'IN', + ); + } + break; - $query = new \WP_Query($args); - $products = []; + case 'sku': + if ( ! empty( $criteria['skus'] ) ) { + $skus = array_map( 'trim', explode( ',', $criteria['skus'] ) ); + $args['meta_query'] = array( + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ), + ); + } + break; + } - if ($query->have_posts()) { - foreach ($query->posts as $post) { - $product = wc_get_product($post->ID); + $query = new \WP_Query( $args ); + $products = array(); - if (!$product) { - continue; - } + if ( $query->have_posts() ) { + foreach ( $query->posts as $post ) { + $product = wc_get_product( $post->ID ); - // Handle variable products by including their variations - if ($product->is_type('variable')) { - $variation_ids = $product->get_children(); - foreach ($variation_ids as $variation_id) { - $variation = wc_get_product($variation_id); - if ($variation && ($include_unpublished || $variation->is_purchasable())) { - $products[] = $variation; - } - } - } elseif ($include_unpublished || $product->is_purchasable()) { - $products[] = $product; - } - } - } + if ( ! $product ) { + continue; + } - wp_reset_postdata(); + // Handle variable products by including their variations + if ( $product->is_type( 'variable' ) ) { + $variation_ids = $product->get_children(); + foreach ( $variation_ids as $variation_id ) { + $variation = wc_get_product( $variation_id ); + if ( $variation && ( $include_unpublished || $variation->is_purchasable() ) ) { + $products[] = $variation; + } + } + } elseif ( $include_unpublished || $product->is_purchasable() ) { + $products[] = $product; + } + } + } - return $products; - } + wp_reset_postdata(); - /** - * Calculate price based on selected products - * - * @param array $selected_products Array of product IDs - * @return float - */ - public function calculate_composed_price($selected_products) { - $pricing_mode = $this->get_pricing_mode(); + return $products; + } - if ($pricing_mode === 'fixed') { - return floatval($this->get_regular_price()); - } + /** + * Calculate price based on selected products + * + * @param array $selected_products Array of product IDs + * @return float + */ + public function calculate_composed_price( $selected_products ) { + $pricing_mode = $this->get_pricing_mode(); - $total = 0; - foreach ($selected_products as $product_id) { - $product = wc_get_product($product_id); - if ($product) { - $total += floatval($product->get_price()); - } - } + if ( 'fixed' === $pricing_mode ) { + return floatval( $this->get_regular_price() ); + } - return $total; - } + $total = 0; + foreach ( $selected_products as $product_id ) { + $product = wc_get_product( $product_id ); + if ( $product ) { + $total += floatval( $product->get_price() ); + } + } - /** - * Add to cart validation - * - * @param int $product_id Product ID - * @param int $quantity Quantity - * @param int $variation_id Variation ID - * @param array $variations Variations - * @param array $cart_item_data Cart item data - * @return bool - */ - public function add_to_cart_validation($product_id, $quantity, $variation_id = 0, $variations = [], $cart_item_data = []) { - return true; - } + return $total; + } + + /** + * Add to cart validation + * + * @param int $product_id Product ID + * @param int $quantity Quantity + * @param int $variation_id Variation ID + * @param array $variations Variations + * @param array $cart_item_data Cart item data + * @return bool + */ + public function add_to_cart_validation( $product_id, $quantity, $variation_id = 0, $variations = array(), $cart_item_data = array() ) { + return true; + } } diff --git a/includes/StockManager.php b/includes/StockManager.php index fada3c4..cc79499 100644 --- a/includes/StockManager.php +++ b/includes/StockManager.php @@ -7,7 +7,7 @@ namespace Magdev\WcComposableProduct; -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; /** * Stock Manager Class @@ -20,15 +20,15 @@ class StockManager { */ public function __construct() { // Hook into order completion to reduce stock - add_action('woocommerce_order_status_completed', [$this, 'reduce_stock_on_order_complete'], 10, 1); - add_action('woocommerce_order_status_processing', [$this, 'reduce_stock_on_order_complete'], 10, 1); + add_action( 'woocommerce_order_status_completed', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 ); + add_action( 'woocommerce_order_status_processing', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 ); // Hook into order cancellation/refund to restore stock - add_action('woocommerce_order_status_cancelled', [$this, 'restore_stock_on_order_cancel'], 10, 1); - add_action('woocommerce_order_status_refunded', [$this, 'restore_stock_on_order_cancel'], 10, 1); + add_action( 'woocommerce_order_status_cancelled', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 ); + add_action( 'woocommerce_order_status_refunded', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 ); // Prevent double stock reduction - add_filter('woocommerce_can_reduce_order_stock', [$this, 'prevent_composable_stock_reduction'], 10, 2); + add_filter( 'woocommerce_can_reduce_order_stock', array( $this, 'prevent_composable_stock_reduction' ), 10, 2 ); } /** @@ -38,42 +38,42 @@ class StockManager { * @param int $quantity Quantity of composable product being added * @return bool|string True if in stock, error message otherwise */ - public function validate_stock_availability($selected_product_ids, $quantity = 1) { - foreach ($selected_product_ids as $product_id) { - $product = wc_get_product($product_id); + public function validate_stock_availability( $selected_product_ids, $quantity = 1 ) { + foreach ( $selected_product_ids as $product_id ) { + $product = wc_get_product( $product_id ); - if (!$product) { + if ( ! $product ) { continue; } // Skip stock check if stock management is disabled for this product - if (!$product->managing_stock()) { + if ( ! $product->managing_stock() ) { continue; } $stock_quantity = $product->get_stock_quantity(); // Check if product is in stock - if (!$product->is_in_stock()) { + if ( ! $product->is_in_stock() ) { return sprintf( /* translators: %s: product name */ - __('"%s" is out of stock and cannot be selected.', 'wc-composable-product'), + __( '"%s" is out of stock and cannot be selected.', 'wc-composable-product' ), $product->get_name() ); } // Check if enough stock is available - if ($stock_quantity !== null && $stock_quantity < $quantity) { + if ( null !== $stock_quantity && $stock_quantity < $quantity ) { return sprintf( /* translators: 1: product name, 2: stock quantity */ - __('Only %2$d of "%1$s" are available in stock.', 'wc-composable-product'), + __( 'Only %2$d of "%1$s" are available in stock.', 'wc-composable-product' ), $product->get_name(), $stock_quantity ); } // Check for backorders - if ($product->backorders_allowed()) { + if ( $product->backorders_allowed() ) { continue; } } @@ -88,29 +88,29 @@ class StockManager { * @param int $required_quantity Required quantity * @return array Stock information [in_stock, stock_quantity, backorders_allowed] */ - public function get_product_stock_info($product_id, $required_quantity = 1) { - $product = wc_get_product($product_id); + public function get_product_stock_info( $product_id, $required_quantity = 1 ) { + $product = wc_get_product( $product_id ); - if (!$product) { - return [ - 'in_stock' => false, - 'stock_quantity' => 0, + if ( ! $product ) { + return array( + 'in_stock' => false, + 'stock_quantity' => 0, 'backorders_allowed' => false, - 'stock_status' => 'outofstock', - ]; + 'stock_status' => 'outofstock', + ); } $stock_quantity = $product->get_stock_quantity(); $managing_stock = $product->managing_stock(); - return [ - 'in_stock' => $product->is_in_stock(), - 'stock_quantity' => $stock_quantity, + return array( + 'in_stock' => $product->is_in_stock(), + 'stock_quantity' => $stock_quantity, 'backorders_allowed' => $product->backorders_allowed(), - 'stock_status' => $product->get_stock_status(), - 'managing_stock' => $managing_stock, - 'has_enough_stock' => !$managing_stock || $stock_quantity === null || $stock_quantity >= $required_quantity, - ]; + 'stock_status' => $product->get_stock_status(), + 'managing_stock' => $managing_stock, + 'has_enough_stock' => ! $managing_stock || null === $stock_quantity || $stock_quantity >= $required_quantity, + ); } /** @@ -118,54 +118,54 @@ class StockManager { * * @param int $order_id Order ID */ - public function reduce_stock_on_order_complete($order_id) { - $order = wc_get_order($order_id); + public function reduce_stock_on_order_complete( $order_id ) { + $order = wc_get_order( $order_id ); - if (!$order) { + if ( ! $order ) { return; } // Check if stock has already been reduced - if ($order->get_meta('_composable_stock_reduced', true)) { + if ( $order->get_meta( '_composable_stock_reduced', true ) ) { return; } - foreach ($order->get_items() as $item) { + foreach ( $order->get_items() as $item ) { $product = $item->get_product(); - if (!$product || $product->get_type() !== 'composable') { + if ( ! $product || $product->get_type() !== 'composable' ) { continue; } // Get selected products from order item meta - $selected_products = $item->get_meta('_composable_products', true); + $selected_products = $item->get_meta( '_composable_products', true ); - if (empty($selected_products) || !is_array($selected_products)) { + if ( empty( $selected_products ) || ! is_array( $selected_products ) ) { continue; } $quantity = $item->get_quantity(); // Reduce stock for each selected product - foreach ($selected_products as $product_id) { - $selected_product = wc_get_product($product_id); + foreach ( $selected_products as $product_id ) { + $selected_product = wc_get_product( $product_id ); - if (!$selected_product || !$selected_product->managing_stock()) { + if ( ! $selected_product || ! $selected_product->managing_stock() ) { continue; } $stock_quantity = $selected_product->get_stock_quantity(); - if ($stock_quantity !== null) { + if ( null !== $stock_quantity ) { $new_stock = $stock_quantity - $quantity; - $selected_product->set_stock_quantity($new_stock); + $selected_product->set_stock_quantity( $new_stock ); $selected_product->save(); // Add order note $order->add_order_note( sprintf( /* translators: 1: product name, 2: quantity, 3: remaining stock */ - __('Stock reduced for "%1$s": -%2$d (remaining: %3$d)', 'wc-composable-product'), + __( 'Stock reduced for "%1$s": -%2$d (remaining: %3$d)', 'wc-composable-product' ), $selected_product->get_name(), $quantity, $new_stock @@ -176,7 +176,7 @@ class StockManager { } // Mark stock as reduced - $order->update_meta_data('_composable_stock_reduced', true); + $order->update_meta_data( '_composable_stock_reduced', true ); $order->save(); } @@ -185,54 +185,54 @@ class StockManager { * * @param int $order_id Order ID */ - public function restore_stock_on_order_cancel($order_id) { - $order = wc_get_order($order_id); + public function restore_stock_on_order_cancel( $order_id ) { + $order = wc_get_order( $order_id ); - if (!$order) { + if ( ! $order ) { return; } // Check if stock was reduced - if (!$order->get_meta('_composable_stock_reduced', true)) { + if ( ! $order->get_meta( '_composable_stock_reduced', true ) ) { return; } - foreach ($order->get_items() as $item) { + foreach ( $order->get_items() as $item ) { $product = $item->get_product(); - if (!$product || $product->get_type() !== 'composable') { + if ( ! $product || $product->get_type() !== 'composable' ) { continue; } // Get selected products from order item meta - $selected_products = $item->get_meta('_composable_products', true); + $selected_products = $item->get_meta( '_composable_products', true ); - if (empty($selected_products) || !is_array($selected_products)) { + if ( empty( $selected_products ) || ! is_array( $selected_products ) ) { continue; } $quantity = $item->get_quantity(); // Restore stock for each selected product - foreach ($selected_products as $product_id) { - $selected_product = wc_get_product($product_id); + foreach ( $selected_products as $product_id ) { + $selected_product = wc_get_product( $product_id ); - if (!$selected_product || !$selected_product->managing_stock()) { + if ( ! $selected_product || ! $selected_product->managing_stock() ) { continue; } $stock_quantity = $selected_product->get_stock_quantity(); - if ($stock_quantity !== null) { + if ( null !== $stock_quantity ) { $new_stock = $stock_quantity + $quantity; - $selected_product->set_stock_quantity($new_stock); + $selected_product->set_stock_quantity( $new_stock ); $selected_product->save(); // Add order note $order->add_order_note( sprintf( /* translators: 1: product name, 2: quantity, 3: new stock */ - __('Stock restored for "%1$s": +%2$d (total: %3$d)', 'wc-composable-product'), + __( 'Stock restored for "%1$s": +%2$d (total: %3$d)', 'wc-composable-product' ), $selected_product->get_name(), $quantity, $new_stock @@ -243,7 +243,7 @@ class StockManager { } // Mark stock as restored - $order->update_meta_data('_composable_stock_reduced', false); + $order->update_meta_data( '_composable_stock_reduced', false ); $order->save(); } @@ -255,11 +255,11 @@ class StockManager { * @param \WC_Order $order Order object * @return bool */ - public function prevent_composable_stock_reduction($reduce_stock, $order) { - foreach ($order->get_items() as $item) { + public function prevent_composable_stock_reduction( $reduce_stock, $order ) { + foreach ( $order->get_items() as $item ) { $product = $item->get_product(); - if ($product && $product->get_type() === 'composable') { + if ( $product && $product->get_type() === 'composable' ) { // We'll handle stock reduction manually return false; } @@ -275,9 +275,9 @@ class StockManager { * @param string $cart_item_key Cart item key * @param array $values Cart item values */ - public function store_selected_products_in_order($item, $cart_item_key, $values) { - if (isset($values['composable_products']) && !empty($values['composable_products'])) { - $item->add_meta_data('_composable_products', $values['composable_products'], true); + public function store_selected_products_in_order( $item, $cart_item_key, $values ) { + if ( isset( $values['composable_products'] ) && ! empty( $values['composable_products'] ) ) { + $item->add_meta_data( '_composable_products', $values['composable_products'], true ); } } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..b4f0049 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,34 @@ + + + PHPCS rules for WooCommerce Composable Products plugin + + + includes + wc-composable-product.php + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 00ac83f..9036a26 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,11 @@ @@ -16,9 +13,9 @@ - + includes - + diff --git a/wc-composable-product.php b/wc-composable-product.php index c36a46a..953130e 100644 --- a/wc-composable-product.php +++ b/wc-composable-product.php @@ -17,74 +17,80 @@ * WC tested up to: 10.0 */ -defined('ABSPATH') || exit; +defined( 'ABSPATH' ) || exit; // Define plugin constants -define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1'); -define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__); -define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__)); -define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__)); -define('WC_COMPOSABLE_PRODUCT_BASENAME', plugin_basename(__FILE__)); +define( 'WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1' ); +define( 'WC_COMPOSABLE_PRODUCT_FILE', __FILE__ ); +define( 'WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path( __FILE__ ) ); +define( 'WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url( __FILE__ ) ); +define( 'WC_COMPOSABLE_PRODUCT_BASENAME', plugin_basename( __FILE__ ) ); // Load Composer autoloader -if (file_exists(WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php')) { - require_once WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php'; +if ( file_exists( WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php' ) ) { + require_once WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php'; } /** * Check if WooCommerce is active */ function wc_composable_product_check_woocommerce() { - if (!class_exists('WooCommerce')) { - add_action('admin_notices', function() { - ?> -
-

-
- +
+

+
+ true) - ); - } + if ( ! class_exists( 'WooCommerce' ) ) { + deactivate_plugins( WC_COMPOSABLE_PRODUCT_BASENAME ); + wp_die( + esc_html__( 'This plugin requires WooCommerce to be installed and active.', 'wc-composable-product' ), + esc_html__( 'Plugin Activation Error', 'wc-composable-product' ), + array( 'back_link' => true ) + ); + } } -register_activation_hook(__FILE__, 'wc_composable_product_activate'); +register_activation_hook( __FILE__, 'wc_composable_product_activate' );